1 poin oleh GN⁺ 2024-08-23 | 1 komentar | Bagikan ke WhatsApp
  • Python dapat menggunakan komentar ajaib encoding sumber pada dua baris pertama dan codec kustom untuk mengubah isi file sebelum dieksekusi, atau menggantinya sepenuhnya dengan kode lain
  • Codec kustom dapat didaftarkan saat inisialisasi interpreter melalui eksekusi import dari file konfigurasi path .pth, dan menambahkan fungsi pencarian dengan codecs.register
  • Implementasi codec memerlukan decode(data: bytes) -> tuple[str, int] dan decoder inkremental; jika exception tidak ditangani, yang mungkin terlihat hanya SyntaxError: encoding problem: your_codec, bukan penyebabnya
  • Operator increment/decrement ++/--, Python berbasis kurung kurawal, eksekusi C/C++ lewat cppyy, hingga validasi TOML dengan JSON Schema dapat diimplementasikan melalui entry point yang sama
  • Selain contoh yang bersifat main-main, ini juga dapat digunakan untuk ekstensi dan backport Python seperti pythonql, future-typing, future-fstrings, future-annotations; magic_codec mengurangi pekerjaan berulang

Menggunakan encoding sumber sebagai entry point preproses

  • Sesuai PEP-0263, salah satu dari dua baris pertama file Python dapat menentukan encoding kode sumber
    • Contoh: # coding=utf8, # -*- coding: utf8 -*-, # vim: set fileencoding=utf8 :
  • Baris ajaib harus cocok dengan regex ^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)
    • Nama codec harus cocok dengan [-_.a-zA-Z0-9]+
  • Codec kustom tidak hanya mendecode sumber, tetapi juga dapat mengubah string sumber lalu meneruskannya ke interpreter Python

Mendaftarkan codec dengan file .pth

  • Jika interpreter Python dijalankan tanpa opsi -S, paket site dimuat saat inisialisasi
  • File konfigurasi path .pth di dalam site-packages menambahkan isi selain baris kosong dan baris yang diawali # ke path pencarian modul
  • Menurut dokumentasi Python, baris yang diikuti spasi atau tab setelah import akan dieksekusi
    • Contoh: jika packagename.pth berisi import packagename.register_codec, modul tersebut akan di-import saat inisialisasi
  • Modul yang di-import dapat memanggil codecs.register untuk mendaftarkan fungsi pencarian codec
    • Karena import hanya dijalankan sekali, fungsi pencarian juga hanya didaftarkan sekali

Mengimplementasikan codec kustom

  • Codec kustom membutuhkan dua hal
    • decode(data: bytes) -> tuple[str, int]
    • Kelas decoder inkremental
  • Fungsi decode dapat melakukan decoding UTF-8 sebenarnya dengan codecs.utf_8_decode, lalu meneruskan string hasilnya ke fungsi preproses
  • Jika exception di dalam codec tidak ditangkap, alih-alih traceback biasa, yang mungkin tercetak hanya SyntaxError: encoding problem: your_codec
    • Exception yang terjadi di fungsi preproses sebaiknya dicetak langsung dengan traceback.print_exc(), lalu di-raise kembali
  • Decoder inkremental dapat mengumpulkan seluruh file di buffer dan melakukan preproses hanya sekali pada pemanggilan decode terakhir
    • Contoh implementasinya mewarisi codecs.BufferedIncrementalDecoder dan memproses hanya saat final bernilai true di decode(self, data, final=False)
  • Hasil preproses tidak harus menggunakan isi file asli, dan boleh mengembalikan kode Python sepenuhnya arbitrer
    • Namun, baris pertama akan dihapus karena dianggap sebagai baris ajaib, dan hasilnya harus berupa Python yang valid

Contoh ekstensi sintaks Python

  • Operator increment dan decrement ++ dan --

    • Python tidak memiliki operator increment/decrement unary
    • x++, x-- tidak valid secara sintaksis
    • ++x, --x valid secara sintaksis, tetapi masing-masing menjadi pemanggilan x.__pos__().__pos__() dan x.__neg__().__neg__()
    • Preprosesor dapat mengubah stream token agar berperilaku seperti operator increment/decrement
      • x++(x, x := x + 1)[0]
      • x--(x, x := x - 1)[0]
      • ++x(x, x := x + 1)[1]
      • --x(x, x := x - 1)[1]
    • Transformasi ini menggunakan operator walrus, yaitu ekspresi assignment Python
    • Sekadar substitusi token dapat gagal pada ekspresi seperti x++ - -y; ambiguitas dapat dikurangi dengan tanda kurung, seperti x++ - (-y)
    • incdec.py melakukan substitusi dengan regex, tetapi bisa tetap rapuh meski mencoba menghindari substitusi di dalam literal string
    • Implementasi yang memodifikasi stream token secara langsung ada di magic.incdec
  • Python berbasis kurung kurawal

    • from __future__ import braces menghasilkan SyntaxError: not a chance
    • Preprosesor dapat memodifikasi stream token untuk mengubah scope kurung kurawal menjadi Python berbasis indentasi
    • Alur implementasinya sebagai berikut
      • Menghasilkan token dengan tokenize.generate_tokens
      • String input diberikan baris demi baris melalui readline dari io.StringIO
      • Token INDENT dan DEDENT yang ada dihapus
      • Saat menemukan {, level indentasi dinaikkan dan : dicetak
      • Saat menemukan }, level indentasi diturunkan
      • Setelah NL, token INDENT yang sesuai dengan level indentasi saat ini ditambahkan
    • Untuk mengurangi benturan dengan literal dictionary Python, level indentasi dapat disesuaikan hanya ketika setelah { adalah newline, dan } diperlakukan sebagai penutup scope hanya ketika sebelumnya newline
    • Dictionary multi-baris dapat digunakan dengan backslash karena tidak menimbulkan token newline di dalam kurung kurawal

Menjalankan bahasa lain sebagai Python

  • C dan C++

    • Bahasa seperti shell script, script CMake, PHP, dan Ruby yang memakai # untuk komentar mudah diberi baris ajaib encoding bersama shebang
    • C dan C++ memakai komentar /* ... */ atau // ..., tetapi karena directive preprosesor diawali #, keduanya dapat dibuat cocok dengan regex encoding
    • Contoh baris ajaib berikut valid di sumber C/C++ sekaligus cocok dengan pola encoding Python
      • #define CODEC "coding:magic.cpp"
    • Dengan cppyy, kode C/C++ dapat diinterpretasikan dari Python dan binding dapat dibuat
      • cppyy menggunakan cling secara internal
    • Hasil preproses kira-kira menjadi kode Python berikut
      • import cppyy
      • cppyy.cppdef("<input source file content>")
      • from cppyy.gbl import main
      • Memanggil main() C/C++ saat __name__ == "__main__"
    • Contoh implementasinya ada di magic.cpp

Menggunakannya sebagai alat validasi TOML

  • Karena TOML memulai komentar dengan #, baris ajaib encoding seperti # coding: magic.toml dapat dimasukkan
  • Jika hasil preproses diganti menjadi skrip validasi Python, interpreter Python dapat digunakan seperti alat validasi TOML
  • Contoh validasi menggunakan modul berikut
    • Membaca file TOML dengan tomllib
    • Membaca file JSON Schema dengan json
    • Melakukan validasi dengan jsonschema
  • Contoh eksekusi:
    • python tests/toml/data_valid.toml -s tests/toml/schema.json
    • Jika valid, mencetak Successfully validated.
  • Pada contoh TOML yang salah, muncul error validasi bahwa string '20' di dalam array scores bukan angka
  • Contoh implementasinya ada di magic.toml

Pemanfaatan nyata dan magic_codec

  • Jika codec kustom dan file .pth digunakan bersama, perilaku interpreter Python dapat diubah secara signifikan
  • Sebagian besar contoh dibuat untuk kesenangan, tetapi ada juga kasus penggunaan nyata
  • Jika tidak ingin menyentuh site-packages secara langsung atau menulis sendiri file .pth dan kode berulang, Anda dapat menggunakan magic_codec
  • Ekstensi magic_codec dapat dibuat sebagai paket Python dengan prefiks magic_
    • Jika codec file disetel ke magic_foo, paket magic_foo akan dimuat
    • Memeriksa apakah paket tersebut memiliki fungsi preprocess
  • Signature preprocess yang diharapkan adalah sebagai berikut
    • def preprocess(data: str) -> str:
  • Contoh ekstensi ada di example/

1 komentar

 
GN⁺ 2024-08-23
Komentar Hacker News
  • Pesan error lucu SyntaxError: not a chance yang muncul saat menjalankan from __future__ import braces ternyata sudah di-hardcode di CPython sejak 2001
    https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
    Penulisnya, Jeremy Hylton, kini bekerja di Google sebagai Principal Engineer yang menangani kualitas pencarian AI. Cukup mengesankan bahwa dalam 24 tahun kariernya berlanjut dari merayakan sintaks terlarang secara iseng ke sistem kueri universal yang tidak membutuhkan sintaks khusus

    • Ini mengingatkan pada kejadian break rust; yang memicu internal compiler error di compiler Rust. Saya jadi penasaran ada berapa banyak easter egg serupa di bahasa lain
    • Saya tidak tahu kenapa itu mengejutkan. Pada 2001 bukan berarti sembarang orang bisa memasukkan sesuatu ke Python; saat itu Python adalah proyek niche, dan orang-orang yang berkontribusi cerdas serta berdedikasi, sehingga kemungkinan besar akan punya karier berpengaruh
      Menurut saya keliru jika menganggap hacking hobi yang iseng dan informal sebagai dunia yang terpisah dari pengembangan profesional sungguhan
    • Masa yang polos. Hylton mungkin saja ikut gerakan mosi tidak percaya demi definisi Tim Peters
      https://news.ycombinator.com/item?id=41314393
    • Melihat easter egg seperti ini selalu keren. Sayang sekarang tidak sesering dulu
  • Saya kira bermain-main dengan import hook adalah cara paling kreatif untuk dipecat, tapi ternyata itu pikiran yang naif. Sayang sepertinya regex codec membuat kita tidak bisa benar-benar melakukan trolling dengan sesuatu seperti μtf8, jadi sekarang tidak ada pilihan selain memakai import hook, preprocessor, dan sys.settrace untuk melakukan monkey patch pada semua fungsi agar menjadi fungsi yang dipanggil tepat sebelumnya, serta menukar stdout dan stderr setiap 17 menit

    • Sebagai bahasa yang baik, harus juga memaksa penggunaan kurung kurawal
  • Ada alasan bagus mengapa Python sengaja tidak mengekspos hook preprocessor, dan menurut saya orang dewasa yang rasional sebaiknya menjauhinya
    Namun di sisi lain, saya tidak ingin berurusan dengan orang dewasa yang rasional. Sepertinya banyak hal yang benar-benar menyenangkan bisa dilakukan

    • Python punya filosofi bahwa ini adalah bahasa untuk “orang dewasa yang sama-sama setuju” (consenting adults), sehingga tidak ada penentu visibilitas public/private, dan mungkin segala macam sihir metaprogramming juga dibiarkan terbuka. Kalau mereka mengkhawatirkan “orang dewasa yang rasional”, keputusan desain seperti ini akan terasa agak aneh :)
    • Kalau orang dicegah melakukan ini dengan cara yang mudah dan jelas, mereka akan mencobanya dengan cara yang lebih buruk dan lebih hacky
  • Ini terlihat praktis dan sangat berguna. Saat melakukan import hacking yang absurd, biasanya saya mengimpor modul, menulis ulang kodenya dengan modul ast, lalu exec dan menyisipkan exit(); dengan preprocessor, ini akan jauh lebih nyaman dipakai
    Sebelum semua dict menjamin urutan, saya terutama memakainya untuk menulis ulang AST agar literal list diubah menjadi pemanggilan ordered dict, dan itu memang berguna
    Saya suka fleksibilitas Python. Hal paling terkutuk yang pernah saya lakukan adalah mengubah string di tempat, dan akhirnya saya sampai menyalahgunakan mmap untuk menulis skrip yang memodifikasi dirinya sendiri. Sekarang rasanya saya harus menulis interpreter Lisp sebagai producer

    • “Mengubah string di tempat”? String itu immutable. Apakah maksudnya menulis langsung ke lokasi memori dengan sesuatu seperti ctypes?
  • Penggunaan terbaik yang saya temukan sejauh ini adalah pyxl yang terinspirasi JSX: https://github.com/dropbox/pyxl
    Kita bisa menulis kode seperti ini
    # coding: pyxl
    print Hello World!

  • Saya penasaran apakah ini bisa dipakai untuk menangani transisi dari Python 2 ke 3 dengan lebih baik. Misalnya # coding: six.python2 menyesuaikan kode Python 2 agar menjadi kode Python 3 yang valid, atau # coding: six.python3 mengubah kode Python 3 agar bisa berjalan di Python 2
    Sepertinya menambahkan atau menghapus prefiks b"..." atau u"..." juga mungkin dilakukan

    • Itu bisa membantu, tetapi bagian yang terbantu adalah bagian yang mudah. Kesulitan migrasi dari Python 2 ke 3 ada pada perubahan perilaku runtime. Di Python 2, Unicode berisi ASCII dan string biasa berperilaku seperti string yang sama, sehingga saat dipakai sebagai key dict keduanya menunjuk ke item yang sama; di Python 3, bytes dan str dengan isi ASCII yang sama menunjuk ke item berbeda dalam dict yang sama
      Ada perubahan yang lebih rumit juga. Banyak fitur bawaan seperti .keys() dan .values() mengembalikan list di Python 2, tetapi mengembalikan iterator di Python 3. Jika mencoba menerjemahkan kode secara aman dengan utilitas six atau workaround lain, kodenya menjadi sangat verbose. Sebagian besar hanya dipakai sekali, tetapi kadang ada yang dipakai dua kali
      Jika ada alat yang bisa menulis ulang kode saat import, menurut saya lebih baik commit saja kode yang sudah dikonversi lalu rapikan secara bertahap. Bagian sulitnya adalah perubahan perilaku seperti str versus bytes yang memengaruhi kode yang berjauhan
  • Apakah dependensi yang muncul dari strategi coding hook ini bisa dideteksi oleh pip freeze atau uv?
    Kalau tidak, bakal jadi waktu yang menyenangkan :). Jika seseorang sudah memasukkan hal seperti ini, hampir pasti ada jebakan lain juga, jadi rasanya lebih mudah menulis ulang library daripada melawan naga-naga semacam itu

  • Sepertinya cukup lucu kalau membuat Python pseudocode, lalu membiarkan LLM mendekodenya. Tentu saja bakal mengerikan, tapi mungkin menyenangkan