آدرس فایل‌ها در پایتون، وقتی پکیج می‌شوند


تیتر سوال خیلی عجیبه قبول دارم!

من امروز داشتم برای خودم یه مقدار کد می‌نوشتم، توی بخشی از کدم لازم بود تا از پوشه‌ای به اسم /templates یک پوشه‌ای رو کپی بکنم. پر واضحه که توی پایتون باید از پت‌های ابسلوت به شکل درست‌اش استفاده کرد تا وقتی کد رو به جای دیگه‌ای منتقل می‌کنیم، همچنان درست کار بکنه و ادرس‌ها درست کار بکنه. کلیت کد من یه همچین چیزی بود:

TEMPLATE_PATH = os.path.join(os.path.abspath("."), "templates/")

این کد، ادرس فعلی جایی که کد رو فراخوانی کردیم رو میاره، اسم /templates رو میذاره اخرش و یه پت درست بهمون میده. کد در حالت عادی که باهاش کار می‌کردم کار می‌کرد، ولی وقتی داشتم براش تست می‌نوشتم و کد رو از جای دیگه‌ای اجرا می‌کردم، متوجه شدم که ارور می‌گیره کد. در حقیقت مشکل این بود که کد بالا، در صورتی که خود فایلی که کد درش قرار داره اجرا بشه درست کار میکنه و در باقی موارد(مثلا ایمپورت کردن این فایل در یک ماژول دیگه) دیگه درست کار نخواهد کرد.

مثلا من ماژولی که این کد درش قرار داشت رو از یک پوشه قبل‌تر نتونستم درست صدا بزنم و بعد که این مشکل رو حل کردم، متوجه شدم که مشکل از این هم بیشتره!

اگر کد بالا رو پکیج کنیم و نصبش کنیم و بعد بخوایم از هرجایی که دلمون میخواد صداش بزنیم، اتفاقی که می‌افته این هستش که بخش اول تابع یعنی (os.path.abspath) شروع میکنه به برگردوندن ادرس جایی که الان درش هستیم. و جایی که درش هستیم دیگه پوشه templates کنار دستش نیست که اونها رو لود بکنه!

برای حل این مشکل، پایتون کلی راه‌حل تر و تمیز و حرفه‌ای داره. از importlib گرفته که از پایتون ۳.۷ به استاندارد لایبرری استفاده شده تا pathlib (باز هم در استاندارد لایبرری).

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

distutils.errors.DistutilsFileError: cannot copy tree '/home/senaps/Projects/python/sfgen/code/templates/simple_app': not a directory

همینجور که می‌بینید، ادرس به صورت کلی از کار افتاده و ادرس در حقیقت باید به همچین فرمتی ساخته می‌شد:

/home/senaps/Projects/python/sfgen/code/sfgen/templates/simple_app

در حقیقت بخاطر اینکه من از داخل پوشه sfgen کد رو تست می‌کردم و حالا برای نصب و اجرای تست‌های اتوماتیکم از داخل پوشه code تست‌ها رو اجرا می‌کردم، دیگه نمی‌تونست درست محل رو پیدا بکنه. اگر هم که بعد از نصب، از مثلا /home اجرا می‌کردم کد رو، ادرس به کلی نابود می‌شد و مثلا همچین فرمتی در می‌اومد:

/home/senaps/templates/simple_app

همونجور که گفتم، راه‌حل های مختلفی برای حل این مشکل هست، ولی من به عنوان یه برنامه‌نیوسی که واقعا حوصله ندارم روش‌های درست رو یاد بگیرم، اومدم و دور زدم مشکل رو. راه‌حل درست مورد نظر من، راه‌حلی بود که بتونه تست‌های لوکال خودم (وقتی که خود فایل cli.py رو اجرا می‌کنم از داخل پوشه sfgen) رو هندل کنه، هم تست‌ها رو پاس کنه و هم بعد از نصب همچنان درست کار کنه! بنابراین اومدم تغییر رو به این صورت اعمال کردم:

if __name__ == "__main__":
    curr_path = os.path.abspath(".")
else:
    curr_path = os.path.abspath(__file__)[:-7]
TEMPLATES_PATH = os.path.join(curr_path, "templates/")

خوب، چیکار کردیم؟

اول اومدم بررسی کردم که آیا کد رو دارم مستقیم صدا میزنم یا اینکه از جای دیگه‌ای داره صدا زده میشه؟ اگر خودم مستقیم کد رو صدا زدم که کلا مشکل هست هست و میریم برای ادامه زندگی‌مون.

اما اگر غیر از اینه، ابتدا میایم ابسلوت پت فایلی که اجرا شده رو میگیریم.

curr_path = os.path.abspath(__file__)
/home/senaps/Projects/python/sfgen/code/sfgen/cli.py

این ادرسی که بهمون داده رو ما بخش اخرش رو نمی‌خوایم! یعنی همین ادرس منهای `/cli.py` واقعا کار ما رو راه می‌ندازه. بنابراین به صورت هارد‌کدی با اسلایس کردنش ‍`[:-7]` مشخص می‌کنیم که کل استرینگ رو غیر از ۷ کاراکتر اخر بهمون بده.

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

نکته این هستش که من توی این کد دوبار شرط ‍‍`if __name__ == "__main__":` رو استفاده کردم. یکبار در بالای کد برای این بخش از ساختن ادرس‌ها، و یکبار در پایین کد برای مشخص کردن روال برنامه.

شما راه‌حل بهتری سراغ دارید برای این مشکل؟ ممنون می‌شم که بهم بگید :)

این‌هم کد نهایی در اینجا که یکی از پروژه‌هایی هستش که رسما روال یادگیری پایتون من رو تا اینجا نمایندگی میکنه :)