مهاجرت به موتور جدید Python‑to‑.NET در نسخه ۲۶.۸

معرفی

نسخه ۲۶.۸ موتور متصل‌کنندهٔ پایتون به .NET را جایگزین می‌کند. اجزای رسم به ماژول aspose.slides منتقل شدند.

اگر پس از ارتقا مشکلی دارید، مستقیماً به I Have an Error بروید.

اجزای رسم به aspose.slides منتقل شدند

هفت نوع منتقل شدند. آن‌ها نام، آرگومان و رفتار خود را حفظ می‌کنند:

نوع قبل از ۲۶.۸ ۲۶.۸ و بعد
Point aspose.pydrawing.Point aspose.slides.Point
PointF aspose.pydrawing.PointF aspose.slides.PointF
Size aspose.pydrawing.Size aspose.slides.Size
SizeF aspose.pydrawing.SizeF aspose.slides.SizeF
Rectangle aspose.pydrawing.Rectangle aspose.slides.Rectangle
RectangleF aspose.pydrawing.RectangleF aspose.slides.RectangleF
Color aspose.pydrawing.Color aspose.slides.Color

این هفت نوع تمام محتوای باقی‌ماندهٔ aspose.pydrawing بودند. پس از تغییر مسیر آن‌ها، دیگر هیچ بخشی از کد شما نیازی به ارجاع به aspose.pydrawing ندارد و تمام ایمپورت‌های آن می‌توانند حذف شوند. این همچنین اعتبارسنجی را آسان می‌کند - به Verify the Migration نگاه کنید.

کد قدیمی:

import aspose.slides as slides
import aspose.pydrawing as drawing

with slides.Presentation() as presentation:
    slide = presentation.slides[0]
    shape = slide.shapes.add_auto_shape(slides.ShapeType.RECTANGLE, 50, 50, 200, 100)

    shape.fill_format.fill_type = slides.FillType.SOLID
    shape.fill_format.solid_fill_color.color = drawing.Color.red

    with slide.get_image(drawing.Size(1920, 1080)) as slide_image:
        slide_image.save("slide1.jpeg", slides.ImageFormat.JPEG)

نسخه ۲۶.۸:

import aspose.slides as slides

with slides.Presentation() as presentation:
    slide = presentation.slides[0]
    shape = slide.shapes.add_auto_shape(slides.ShapeType.RECTANGLE, 50, 50, 200, 100)

    shape.fill_format.fill_type = slides.FillType.SOLID
    shape.fill_format.solid_fill_color.color = slides.Color.red

    with slide.get_image(slides.Size(1920, 1080)) as slide_image:
        slide_image.save("slide1.jpeg", slides.ImageFormat.JPEG)

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

# کد قدیمی
from aspose.pydrawing import Color, Point

# نسخه ۲۶.۸
from aspose.slides import Color, Point

رفع خطای ایمپورت

ردیابی خطا (traceback) خود را در ستون اول پیدا کنید.

خطا دلیل راه‌حل
AttributeError: module 'aspose.pydrawing' has no attribute 'Color' (یا Point، Rectangle و غیره) پکیج ۲۶.۸ است، اما کد هنوز به ماژول قدیمی اشاره می‌کند Update your code
ImportError: cannot import name 'Color' from 'aspose.pydrawing' همین دلیل، نحو ایمپورت from Update your code
ModuleNotFoundError: No module named 'aspose.pydrawing' ماژول و همهٔ هفت نوع آن به aspose.slides منتقل شده‌اند Update your code، سپس ایمپورت aspose.pydrawing را حذف کنید
ImportError: cannot import name 'Color' from 'aspose.slides' کد مهاجرت کرده است، اما پکیج نصب‌شده ۲۶.۷ یا قدیمی‌تر است pip install --upgrade aspose.slides
TypeError on a color, point, or size argument مقداری که از aspose.pydrawing ایجاد شده به API جدید پاس داده شده مقدار را همچنین از aspose.slides ایجاد کنید

به‌روزرسانی کد شما

از آنجا که aspose.pydrawing جز محتوا به جز هفت نوع منتقل شده چیزی ندارد، مهاجرت صرفاً تغییر نام ماژول است. تمام اشکال ایمپورت توسط این تغییر نام تک‌گانه پوشش داده می‌شوند، از جمله مستعارها:

# کد قدیمی
import aspose.pydrawing as drawing
color = drawing.Color.red

# نسخه ۲۶.۸ - مستعار کار می‌کند
import aspose.slides as drawing
color = drawing.Color.red

این در هر دامنه‌ای معتبر است، حتی داخل بدنه یک تابع، چون مستعار دقیقاً در همان جایی که قبلاً بایند شده باقی می‌ماند. تنها نکته منفی نام گمراه‌کننده است، بنابراین در نظر داشته باشید هدف را به طور واضح بیان کنید:

import aspose.slides as slides
color = slides.Color.red

روشی را انتخاب کنید که متناسب با اندازهٔ پایه کد شما باشد.

جایگزینی دستی

برای تعداد کمی فایل، به‌دنبال aspose.pydrawing بگردید و آن را با aspose.slides جایگزین کنید، سپس هر ایمپورت غیرضروری را حذف کنید.

جایگزینی با فرمان شل

این یک جایگزینی متن ساده است، بنابراین موارد داخل رشته‌ها و نظرات نیز تحت تأثیر قرار می‌گیرد. هر دو فرمان یک نسخهٔ .bak از هر فایل تغییر یافته می‌نویسند.

Linux:

grep -rlZ --include='*.py' 'aspose\.pydrawing' . \
  | xargs -0 -r sed -i.bak 's/aspose\.pydrawing/aspose.slides/g'

در macOS، به جای sed -i.bak از sed -i '' استفاده کنید یا GNU sed را به عنوان gsed نصب کنید.

Windows PowerShell:

Get-ChildItem -Recurse -Filter *.py | ForEach-Object {
  $t = Get-Content $_ -Raw
  $new = $t -replace 'aspose\.pydrawing', 'aspose.slides'
  if ($new -ne $t) {
    Copy-Item $_.FullName "$($_.FullName).bak"
    Set-Content $_.FullName $new -NoNewline
    $_.FullName
  }
}

برای بازگشت در Linux یا macOS:

find . -name '*.py.bak' -exec sh -c 'mv "$1" "${1%.bak}"' _ {} \;

برای بازگشت در Windows:

Get-ChildItem -Recurse -Filter *.py.bak | ForEach-Object {
  Move-Item $_.FullName ($_.FullName -replace '\.bak$', '') -Force
}

جایگزینی با اسکریپت پایتون

همان تغییر نام، قابل حمل بر روی Linux، macOS و Windows. اسکریپت مسیر را به‌عنوان آرگومان می‌گیرد و تغییرات را پیش‌نمایش می‌کند مگر اینکه --write ارائه شود. برای حفظ یک نسخهٔ .bak از هر فایل تغییر یافته --backup را اضافه کنید. آن را تحت هر نامی ذخیره کنید؛ پیام استفاده نام را در زمان اجرا می‌گیرد.

"""تغییر نام aspose.pydrawing به aspose.slides. جایگزینی متن ساده.

    python <this script> src/                     # پیش‌نمایش
    python <this script> src/ --write             # اعمال
    python <this script> src/ --write --backup    # اعمال، حفظ نسخه‌های .bak
"""

import sys
from pathlib import Path

W = "--write" in sys.argv
B = "--backup" in sys.argv
ROOT = next((a for a in sys.argv[1:] if not a.startswith("-")), None)

if ROOT is None:
    sys.exit(f"usage: python {Path(sys.argv[0]).name} <path> [--write] [--backup]")

root = Path(ROOT)
if not root.exists():
    sys.exit(f"no such path: {root}")

files = [root] if root.is_file() else root.rglob("*.py")
changed = 0

for p in files:
    if {".venv", "venv", "__pycache__", ".git"} & set(p.parts):
        continue
    s = p.read_text(encoding="utf-8")
    n = s.replace("aspose.pydrawing", "aspose.slides")
    if n == s:
        continue
    changed += 1
    print(("wrote " if W else "would change ") + str(p))
    if W:
        if B:
            p.with_suffix(p.suffix + ".bak").write_text(s, encoding="utf-8")
        p.write_text(n, encoding="utf-8")

print(f"{changed} file(s) {'changed' if W else 'to change'}"
      + ("" if W or not changed else "; rerun with --write to apply"))

یک اجرا معمولی به این شکل است:

$ python migrate.py src/
would change src/render.py
would change src/export/slides.py
2 file(s) to change; rerun with --write to apply

$ python migrate.py src/ --write --backup
wrote src/render.py
wrote src/export/slides.py
2 file(s) changed

مسیر می‌تواند یک پوشه باشد که به‌صورت بازگشتی پیمایش می‌شود یا یک فایل .py منفرد.

جایگزینی با اسکریپت مبتنی بر AST

برای پایه‌های کد بزرگتر توصیه می‌شود. این اسکریپت همان تغییر نام را انجام می‌دهد، اما ابتدا هر فایل را پارس می‌کند، بنابراین هرگز موارد داخل رشته‌ها، نظرات یا docstringها را تحت تأثیر قرار نمی‌دهد.

از آنجا که ماژول را در‌جا تغییر نام می‌دهد و مستعارها را دست‌نخورده می‌گذارد، تمام اشکال ایمپورت بدون موارد خاص مدیریت می‌شوند: import aspose.pydrawing، import aspose.pydrawing as X، from aspose.pydrawing import Color، from aspose.pydrawing import Color as C، ایمپورت‌های چندخطی داخل پرانتز، ایمپورت‌ها داخل توابع و ماژول به‌عنوان مقدار عبور داده‌شده. همین پرچم‌های --write و --backup را می‌پذیرد.

"""تغییر نام aspose.pydrawing به aspose.slides، عبور از رشته‌ها و نظرات.

    python <this script> src/                     # پیش‌نمایش
    python <this script> src/ --write             # اعمال
    python <this script> src/ --write --backup    # اعمال، حفظ نسخه‌های .bak
"""

import ast, sys
from pathlib import Path

MOD, DST = "aspose.pydrawing", "aspose.slides"
W = "--write" in sys.argv
B = "--backup" in sys.argv
ROOT = next((a for a in sys.argv[1:] if not a.startswith("-")), None)

if ROOT is None:
    sys.exit(f"usage: python {Path(sys.argv[0]).name} <path> [--write] [--backup]")

root = Path(ROOT)
if not root.exists():
    sys.exit(f"no such path: {root}")

files = [root] if root.is_file() else root.rglob("*.py")
changed = 0


def chain(n):
    p = []
    while isinstance(n, ast.Attribute):
        p.append(n.attr)
        n = n.value
    return ".".join(reversed(p + [n.id])) if isinstance(n, ast.Name) else None


def fix(src):
    tree = ast.parse(src)
    off, o = [], 0
    for l in src.encode().splitlines(keepends=True):
        off.append(o)
        o += len(l)
    off.append(o)
    edits = []

    for n in ast.walk(tree):
        # وارد کردن aspose.pydrawing [as X]  /  از aspose.pydrawing import ...
        # نام ماژول درجا تغییر نام می‌یابد، بنابراین هر مستعاری همان‌طور که قبلاً بایند شده باقی می‌ماند.
        if (isinstance(n, ast.Import) and any(a.name == MOD for a in n.names)) or \
           (isinstance(n, ast.ImportFrom) and n.module == MOD):
            s, e = off[n.lineno - 1], off[n.end_lineno - 1] + n.end_col_offset
            edits.append((s, e, src.encode()[s:e].decode().replace(MOD, DST)))
        # هر عبارت خواستار ماژول، از جمله `fn(aspose.pydrawing)` ساده.
        elif isinstance(n, ast.Attribute) and chain(n) == MOD:
            edits.append((off[n.lineno - 1] + n.col_offset,
                          off[n.end_lineno - 1] + n.end_col_offset, DST))

    b = src.encode()
    for s, e, r in sorted(edits, reverse=True):  # back to front keeps offsets valid
        b = b[:s] + r.encode() + b[e:]
    return b.decode()


for p in files:
    if {".venv", "venv", "__pycache__", ".git"} & set(p.parts):
        continue
    s = p.read_text(encoding="utf-8")
    try:
        n = fix(s)
    except SyntaxError as e:
        print(f"skipped {p}: {e}")
        continue
    if n != s:
        print(("wrote " if W else "would change ") + str(p))
        if W:
            if B:
                p.with_suffix(p.suffix + ".bak").write_text(s, encoding="utf-8")
            p.write_text(n, encoding="utf-8")

هر دو اسکریپت ایندومنت هستند: اجرای دوباره آن‌ها روی کد مهاجرت‌شده تغییری ایجاد نمی‌کند.

تأیید مهاجرت

یک جستجوی متن نشان می‌دهد آیا چیزی باقی مانده است یا نه:

grep -rn 'aspose\.pydrawing' --include='*.py' --exclude-dir=.venv .

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

import ast, sys
from pathlib import Path

MOD = "aspose.pydrawing"
ROOT = next((a for a in sys.argv[1:] if not a.startswith("-")), ".")


def chain(n):
    p = []
    while isinstance(n, ast.Attribute):
        p.append(n.attr)
        n = n.value
    return ".".join(reversed(p + [n.id])) if isinstance(n, ast.Name) else None


def scan(tree):
    for n in ast.walk(tree):
        if isinstance(n, ast.Import) and any(a.name == MOD for a in n.names):
            yield n.lineno, f"import {MOD}"
        elif isinstance(n, ast.ImportFrom) and n.module == MOD:
            names = ", ".join(a.name for a in n.names)
            yield n.lineno, f"from {MOD} import {names}"
        elif isinstance(n, ast.Attribute) and chain(n) == MOD:
            yield n.lineno, f"reference to {MOD}"


hits = 0
for p in sorted(Path(ROOT).rglob("*.py")):
    if {".venv", "venv", "__pycache__", ".git"} & set(p.parts):
        continue
    try:
        tree = ast.parse(p.read_text(encoding="utf-8"))
    except SyntaxError as e:
        print(f"skipped {p}: {e}")
        continue
    for lineno, what in sorted(scan(tree)):
        print(f"{p}:{lineno}: {what}")
        hits += 1

print("migration complete" if not hits else f"{hits} reference(s) left")
sys.exit(1 if hits else 0)

قبل و بعد از مهاجرت آن را اجرا کنید:

$ python verify.py src/
src/render.py:4: from aspose.pydrawing import Color, Point
src/render.py:11: import aspose.pydrawing
src/render.py:12: reference to aspose.pydrawing
3 reference(s) left

$ python migrate.py src/ --write
wrote src/render.py

$ python verify.py src/
migration complete

در نهایت، یک تست Smoke اجرا کنید که انواع منتقل‌شده را تست می‌کند:

import aspose.slides as slides

with slides.Presentation() as presentation:
    slide = presentation.slides[0]
    shape = slide.shapes.add_auto_shape(slides.ShapeType.RECTANGLE, 50, 50, 200, 100)

    shape.fill_format.fill_type = slides.FillType.SOLID
    shape.fill_format.solid_fill_color.color = slides.Color.red

    presentation.save("smoke.pptx", slides.export.SaveFormat.PPTX)
    print("OK")

ترتیب پیشنهادی مهاجرت

  1. یک نقطهٔ پایه ذخیره کنید. تست‌های خود را بر روی نسخهٔ فعلی اجرا کنید و رندرهای مرجع را نگه دارید. این به شما اجازه می‌دهد خطاهای مهاجرت را از اختلافات رندر بعداً جدا کنید.
  2. پیشنمایش مهاجرت. یکی از اسکریپت‌ها را بدون --write اجرا کنید و لیست فایل‌هایی که تغییر می‌دهند را بررسی کنید.
  3. اعمال و تأیید. با --write --backup اجرا کنید، سپس اسکریپت تأیید و تست Smoke را اجرا کنید.
  4. رندرها را با تحمل مقایسه کنید. انتقال به ساخت .NET 6 ممکن است تفاوت‌های کوچکی در متن و اثرات ایجاد کند. به جای بررسی بایت به بایت، از مقایسه مبتنی بر آستانه استفاده کنید.
  5. پشتیبان‌ها را حذف کنید. پس از تأیید نتیجه، فایل‌های .bak را حذف کنید: find . -name '*.py.bak' -delete در Linux و macOS، یا Get-ChildItem -Recurse -Filter *.py.bak | Remove-Item در Windows.

پشتیبانی از هر دو نسخه در یک پایه کد

اگر لازم است با نسخه‌های ۲۶.۷ و ۲۶.۸ از یک منبع اجرا کنید:

try:
    from aspose.slides import Color, Point, Rectangle      # ۲۶.۸ و بعدی
except ImportError:
    from aspose.pydrawing import Color, Point, Rectangle   # ۲۶.۷ و قبلی

آنچه تغییر نکرد

  • نام‌ها، آرگومان‌ها و رفتار اجزای منتقل‌شده.
  • باقی‌ماندهٔ سطح API aspose.slides.
  • مجوزها و نحوهٔ اعمال فایل لایسنس.
  • قالب‌های فایل و رفتار ذخیره‌سازی و بارگذاری.
  • نیازمندی‌های سیستم در Windows و macOS.
  • عدم وجود نصب جداگانهٔ .NET - زمان اجرا همچنان بسته‌بندی شده است.