برنامه نویسی

stubgen-pyx: خرد mypy نمی تواند ایجاد کند

خلاصه به زبان فارسی (۱۹۰ کلمه):
انویدیا ابزار stubgen-pyx را برای تولید خودکار فایل‌های *.pyi (type stubs) برای ماژول‌های Cython جعبه‌ابزار CUDA معرفی کرده است. این ابزار با پردازش مستقیم کد منبع Cython (بدون نیاز به باینری کامپایل‌شده)‌، اطلاعات دقیق انواع و مستندات را استخراج می‌کند. این پیشرفت امکان تکمیل خودکار در IDEها، تشخیص باگ‌ها از طریق بررسی ثبات نوع، و پشتیبانی کامل از تایپ‌چکینگ (مانند mypy) را فراهم می‌کند.

مشکل عمده ژنراتورهای قبلی (مانند stubgen)، عدم حفظ جزئیات انواع (مثلاً تبدیل تمام آرگومان‌ها به *args, **kwargs) و از دست رفتن مستندات پس از کامپایل بود. stubgen-pyx با تجزیه AST کد Cython، تبدیل مستقیم انواع (مثل cdef به معادل پایتون)، و حذف ورودی‌های غیرضروری (cimport‌های C، توابع غیرعمومی)، خروجی‌های پاک تولید می‌کند. این ابزار به صورت خطی یا در CI قابل استفاده است و فایل‌های .pyi تولید‌شده را کنار باینری حفظ می‌کند تا کاربران از hỗایت کامل برخوردار شوند. کد پروژه متن‌باز در GitHub موجود است.

cuda-python انویدیا، پیوندهای رسمی پایتون برای جعبه ابزار CUDA، که اخیراً به صورت خودکار ایجاد شده اضافه شده است. .pyi فایل های خرد با استفاده از stubgen-pyx. توضیح آنها از چرایی آن:

“این اجازه می دهد تا تکمیل خودکار IDE کار کند (که توسط عوامل برنامه نویسی یکپارچه IDE نیز استفاده می شود). این همچنین 2 باگ واقعی را در کد ما پیدا کرده است. توانایی یافتن دسته خاصی از اشکالات با این کار در آینده بسیار مفید خواهد بود، به خصوص از آنجایی که توانایی های پرده سازی ما با cython-lint کمی عقب تر از آنچه در پایتون خالص هستند.”

وقتی متعهد می شوید .pyi در کنار یک پسوند Cython، یک مصنوع دریافت می‌کنید که خط لوله معمولی پایتون پرده‌بندی و تایپ بررسی می‌شود. می تواند تجزیه و تحلیل کنید. ناهماهنگی بین آنچه منبع Cython انجام می دهد و آنچه که ادعاهای خرد نشان می دهد قابل مشاهده است. NVIDIA قبل از گزارش دو باگ واقعی از این طریق پیدا کرد.

من نویسنده stubgen-pyx هستم، و این پست یک توضیح فنی از نحوه تولید آن خرد است.


مشکل ژنراتورهای خرد موجود

هنگامی که یک ماژول Cython را کامپایل می کنید، منبع ناپدید می شود. چیزی که به دست می آورید یک است .so (یا .pydفایل: یک پسوند کامپایل شده بدون اطلاعات نوع قابل خواندن توسط سرور زبان. ابزارهایی مانند mypy’s stubgen می‌تواند با وارد کردن باینری کامپایل‌شده و با استفاده از درون‌نگری زمان اجرا، برای آن‌ها خرد تولید کند.

نتایج معمولا ناامید کننده هستند. این ماژول Cython تایپ شده را بگیرید:

"""Mathematical utilities for scientific computing."""

cdef class Matrix:
    """A simple matrix class."""

    cdef int rows
    cdef int cols

    def __init__(self, int rows, int cols):
        """Initialize a matrix."""
        self.rows = rows
        self.cols = cols

    def shape(self) -> tuple[int, int]:
        """Get matrix dimensions."""
        return (self.rows, self.cols)

    cpdef scale(self, double factor):
        """Scale all elements."""
        pass

    cdef int _validate(self):
        """Internal validation (not exposed)."""
        return 0

def matrix_product(Matrix a, Matrix b) -> Matrix:
    """Compute matrix product."""
    return Matrix(a.rows, b.cols)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حال دویدن stubgen در باینری کامپایل شده تولید می کند:

import _cython_3_2_4
matrix_product: _cython_3_2_4.cython_function_or_method
class Matrix:
    def __init__(self, *args, **kwargs) -> None: ...
    def scale(self, *args, **kwargs): ...
    def shape(self, *args, **kwargs): ...
    def __reduce__(self): ...
    def __reduce_cython__(self, *args, **kwargs): ...
    def __setstate_cython__(self, *args, **kwargs): ...
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حاشیه‌نویسی‌هایی که روی هر آرگومان نوشتید، در باینری کامپایل‌شده به شکلی که درون‌نگری می‌تواند بازیابی شود، باقی نمی‌ماند. شما می توانید رشته های اسناد گم شده و تایپ نشده را دریافت کنید *args, **kwargs همه جا

stubgen-pyx رویکرد متفاوتی دارد: هرگز باینری کامپایل شده را لمس نمی کند. منبع Cython را مستقیماً می‌خواند، آن را با استفاده از کامپایلر داخلی Cython تجزیه می‌کند و نوع حاشیه‌نویسی و اسنادی را که نویسنده نوشته است استخراج می‌کند. خروجی برای همان ماژول:

# This file was generated by stubgen-pyx

"""Mathematical utilities for scientific computing."""
from __future__ import annotations


class Matrix:
    """A simple matrix class."""

    def scale(self, factor: float):
        """Scale all elements."""

    def __init__(self, rows: int, cols: int):
        """Initialize a matrix."""

    def shape(self) -> tuple[int, int]:
        """Get matrix dimensions."""

def matrix_product(a: Matrix, b: Matrix) -> Matrix:
    """Compute matrix product."""
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


با استفاده از آن

pip install stubgen-pyx
stubgen-pyx ./your_package
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

یا به صورت برنامه ای:

from stubgen_pyx import StubgenPyx
from stubgen_pyx.config import StubgenPyxConfig

config = StubgenPyxConfig(
    continue_on_error=True,  # don't abort on one bad file
    include_private=True,    # include _private functions
)

stubgen = StubgenPyx(config=config)
results = stubgen.convert_glob("src/**/*.pyx")
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای نگهبانان کتابخانه، الگوی توصیه شده این است که stubgen-pyx را به عنوان مرحله ای در CI انتشار خود اجرا کنید و کامپیت کردن تولید شده .pyi فایل ها در کنار پسوند کامپایل شده شما کاربران پشتیبانی کامل از IDE را بدون ایجاد خرد دریافت می کنند.


خط لوله پنج مرحله ای

جریان کامل از .pyx منبع به .pyi خروجی:

  1. پیش پردازش – منبع را عادی کنید تا تجزیه کننده Cython اعداد خطوط دقیق را گزارش کند
  2. تجزیه AST – خوراک خود Cython parse_from_strings
  3. تحلیل بازدیدکنندگان – توابع جمع آوری AST، کلاس ها، enums، واردات
  4. تبدیل – گره های AST خام را به سطح متوسط ​​ترسیم کنید PyiElement کلاس های داده
  5. ساختمان + پس پردازش – انتشار متن خرد، عادی سازی انواع، اصلاح واردات

مرحله 1: پیش پردازش

کامپایلر Cython اعداد خط دقیقی را برای انواع گره‌های خاص گزارش نمی‌کند و به راه‌حل‌هایی نیاز دارد تا بتوان خطوط را از منبع به نتیجه کپی کرد. .pyi با دقت فایل کنید این امر به ویژه برای بررسی تزیین کنندگان عملکرد/کلاس و بیانیه های تکلیف اهمیت دارد. شش تبدیل رشته متوالی را اجرا می کند که هر کدام از Python استفاده می کنند tokenize ماژول:

  • replace_tabs_with_spaces – هدایت زبانه ها به 4 فاصله
  • remove_comments – همه نشانه های نظر را حذف می کند و با فاصله ها جایگزین می شود
  • collapse_line_continuations – بک اسلش + خط جدید به فاصله
  • remove_contained_newlines – خطوط جدید داخل براکت ها/پرانتز/پرانتزها را حذف می کند که با پشته براکت مبتنی بر توکن ردیابی می شوند
  • expand_colons – تقسیم می شود def f(): return 0 روی خطوط جدید مناسب؛ فقط دونقطه هایی که بلوک ها را باز می کنند
  • expand_semicolons – همان درمان برای نقطه ویرگول

یک جزئیات پیچیده: # type: int نظرات (سبک PEP 484) استخراج می شود قبل از نظرات حذف می شوند و شماره خطوط آنها برای محاسبه خطوط حذف شده توسط مرحله جمع شدن براکت-خط جدید تنظیم می شود.


مراحل 2-3: تجزیه و تحلیل بازدیدکنندگان

پس از پیش پردازش، منبع به خود Cython می رود parse_from_strings، دریافت همان AST یک کامپایل با تمام اطلاعات نوع دست نخورده تولید می کند.

سپس چهار کلاس بازدیدکننده در مسیر AST قدم می‌زنند که همگی Cython را گسترش می‌دهند TreeVisitor:

  • ModuleVisitor – نقطه ورود سطح بالا، به دیگران تفویض می کند
  • ScopeVisitor – توابع، کلاس ها، enums، انتساب ها، متغیرهای cdef را جمع آوری می کند
  • ClassVisitorScopeVisitor با in_class=True
  • ImportVisitor – تمام اظهارات واردات و واردات را جمع آوری می کند

تصمیم کلیدی طراحی این است که چه چیزی جمع آوری شود و چه چیزی نادیده گرفته شود. ScopeVisitor.visit_CFuncDefNode چک ها node.declarator.overridable، یک بولی که جدا می شود cpdef (Python-callable) از خالص cdef (برای واردکنندگان پایتون نامرئی):

def visit_CFuncDefNode(self, node):
    if not node.declarator.overridable:
        return node  # pure cdef — not Python-visible, drop it
    self.cdef_functions.append(node)
    return node
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

عمومی cdef class صفات (cdef public int count) نیز جمع آوری می شوند زیرا آنها در معرض تماس گیرندگان پایتون هستند.

ImportVisitor یک مورد خاص قابل توجه دارد: از آن عبور می کند if TYPE_CHECKING: بلوک ها هر گونه نگهبانان فایل Cython شما را وارد می کند TYPE_CHECKING، یک الگوی رایج برای اجتناب از واردات دایره ای، هنوز هم برداشته می شود و در خرد گنجانده می شود.


مرحله 4: استخراج امضا و PyiElement نمایندگی میانی

را Converter کلاس هر گره AST جمع آوری شده را به یک تبدیل می کند PyiElement کلاس داده این نمایش میانی بین Cython AST و متن خروجی قرار می گیرد: نام تابع یا کلاس، لیست آرگومان، نوع بازگشت، رشته مستندات و تزئینات را به شکل خنثی نگه می دارد که مراحل پس پردازش می توانند بدون دانستن چیزی در مورد ساختار AST Cython با آن کار کنند. این مرز همان چیزی است که اضافه کردن یک پاس پس پردازش جدید بدون دست زدن به تجزیه کننده را آسان می کند.

هسته تبدیل است get_signature، که هر دو را اداره می کند def و cdef/cpdef گره ها آنها ساختارهای اعلام کننده متفاوتی در AST دارند.

برای انواع آرگومان دو منبع وجود دارد. حاشیه نویسی به سبک پایتون (def f(x: int)) از خوانده می شوند arg.annotation.string.value. تایپ سیتون به سبک C (cdef int x) از استخراج شده است arg.base_type از طریق extract_type_from_base_type، که گره نوع پایه را برای بازسازی مسیرهای ماژول نقطه‌دار طی می‌کند.

نشانگرهای فقط موقعیت و فقط کلمه کلیدی حفظ می شوند: سازنده امضا منتشر می کند / و * در مکان های مناسب بر اساس num_posonly_args و num_kwonly_args از AST

Enums درمان ویژه ای دارند. الف cpdef enum (create_wrapper=True) یک کلاس مناسب با intویژگی های تایپ شده یک دشت cdef enum تبدیل می شود MyEnum = int، که دقیق است زیرا C enum فقط اعداد صحیح در مرز پایتون هستند.


مرحله 5: پس پردازش

خرد خام تولید شده، مجموعه ای از PyiElement مقادیر سریال شده به متن، از چندین گذر عبور می کند که هر کدام روی پایتون کار می کنند ast.AST.

عادی سازی نام را تایپ کنید

Cython واژگان نوع خاص خود را دارد که برای چکرهای نوع پایتون معنایی ندارد. نرمال ساز هر نام مخصوص Cython را از طریق an به معادل پایتون خود نگاشت می کند ast.NodeTransformer:

نوع سیتون معادل پایتون
bint bool
unicode str
void None
char، short، long، int8_tuint64_t، Py_ssize_t، size_t int
double، longdouble float
doublecomplex، floatcomplex complex

بنابراین cpdef uint32_t compute(int64_t x) تبدیل می شود def compute(x: int) -> int در خرد همه عرض های اعداد صحیح جمع می شوند int. تماس گیرندگانی که به آنها اهمیت می دهند uint32_t در مقابل int64_t تمایز آنها را نمی بیند، اما پایتون هیچ گونه معادلی ندارد، بنابراین int حاشیه نویسی صادقانه است.

اصلاح واردات

خرد با تمام واردات از اصل شروع می شود .pyx، اما بسیاری از آنها خواهند بود cimports از هدرهای C بدون معنای سمت پایتون. آنها برای وارد کردن تعاریف نوع C مورد نیاز کامپایلر Cython وجود دارند، اما این تعاریف به عنوان ماژول‌های Python قابل واردات وجود ندارند. صاف کننده هر نامی را که واقعاً در یادداشت‌ها و امضاها استفاده می‌شود جمع‌آوری می‌کند، سپس هر گونه وارداتی را که نام استفاده شده را تغذیه نمی‌کند حذف می‌کند.

را cimport خود کلمه کلیدی بازنویسی می شود import توسط PyiImport.__post_init__، از طریق یک جایگزین ساده regex. الف cimport که از پیرایش جان سالم به در می‌برد، تبدیل به یک واردات استاندارد پایتون در مقاله خرد می‌شود.

کپی برداری و مرتب سازی

اظهارات واردات یکسان ادغام می شوند. واردات به ترتیب به سبک isort مرتب شده اند: from __future__ ابتدا، سپس stdlib، سپس شخص ثالث، سپس محلی.


چیزی که آن را اداره نمی کند

نماهای حافظه (double[::1]، int[:, :]) در برخی موارد به حاشیه نویسی های تایپ نشده برگردید. دسته های استخراج نوع پایه CSimpleBaseTypeNode و اساسی TemplatedTypeNode، اما نحو برش چند بعدی یک گره AST پیچیده تر است.

انواع ذوب شده (ctypedef fused numeric: int | double) پشتیبانی اولیه داشته باشد. خرد شامل نام نوع ذوب شده همانطور که هست.

تایپ صحیح 95 درصد امضاها مفیدتر از تلاش برای پوشش کامل و تولید موارد خرد نادرست است.


معماری

پایگاه کد در شش زیر بسته جدا شده است: parsing، analysis، conversion، models، builders، postprocessing، هر کدام یک مسئولیت دارند. را PyiElement نمایش میانی Cython AST را از متن خروجی جدا می‌کند و اضافه کردن پاس‌های پس‌پردازش یا گسترش عادی‌سازی نوع را بدون لمس کدهای نامرتبط آسان می‌کند.


اگر این مفید بود، می‌توانید از من در GitHub حمایت مالی کنید یا برای حمایت از کار من یک قهوه برای من بخرید. بازخورد و مشارکت در github.com/jon-edward/stubgen-pyx خوش آمدید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا