Halo semuanya!
Kami sudah memiliki satu artikel tentang pengembangan pengetikan di Ostrovok.ru . Ini menjelaskan mengapa kita beralih dari pyContracts ke typeguard, mengapa kita beralih ke typeguard dan berakhir dengan apa. Dan hari ini saya akan memberi tahu Anda lebih banyak tentang bagaimana transisi ini terjadi.

Deklarasi fungsi dengan pyContracts umumnya terlihat seperti ini:
from contracts import new_contract import datetime @new_contract def User (x): from models import User return isinstance(x, User) @new_contract def dt_datetime (x): return isinstance(x, datetime.datetime) @contract def func(user_list, amount, dt=None): """ :type user_list: list(User) :type amount: int|float :type dt: dt_datetime|None :rtype: bool """ β¦
Ini adalah contoh abstrak, karena dalam proyek kami saya tidak menemukan definisi fungsi yang pendek dan bermakna dalam hal jumlah kasus untuk pemeriksaan jenis. Biasanya, definisi untuk pyContracts disimpan dalam file yang tidak mengandung logika lain. Perhatikan bahwa di sini Pengguna adalah kelas pengguna tertentu, dan tidak diimpor secara langsung.
Dan ini adalah hasil yang diinginkan dengan typeguard:
from typechecked import typechecked from typing import List, Optional, Union from models import User import datetime @typechecked def func (user_list: List[User], amount: Union[int, float], dt: Optional[datetime.datetime]=None) -> bool: ...
Secara umum, ada begitu banyak fungsi dan metode dengan pengecekan tipe dalam proyek sehingga jika Anda menumpuknya dalam tumpukan, Anda dapat mencapai bulan. Jadi menerjemahkannya secara manual dari pyContracts ke typeguard sama sekali tidak mungkin (saya sudah mencoba!). Jadi saya memutuskan untuk menulis naskah.
Script dibagi menjadi dua bagian: satu cache impor kontrak baru, dan yang kedua berkaitan dengan kode refactoring.
Saya ingin mencatat bahwa tidak satu pun dari skrip lain mengklaim sebagai universal. Kami tidak bermaksud untuk menulis alat untuk menyelesaikan semua kasus yang diperlukan. Oleh karena itu, saya sering menghilangkan pemrosesan otomatis dari beberapa kasus khusus, jika jarang ditemukan dalam proyek, lebih cepat untuk memperbaikinya dengan tangan. Misalnya, skrip untuk menghasilkan kontrak pemetaan dan impor mengumpulkan 90% dari nilai, 10% sisanya adalah pemetaan kerajinan tangan.
Logika skrip untuk menghasilkan pemetaan:
Langkah 1. Telusuri semua file proyek, bacalah. Untuk setiap file:
- jika substring "@new_contract" tidak ada, lewati file ini,
- jika ada, maka bagi file dengan baris "@new_contract". Untuk setiap item:
- parse untuk definisi dan impor,
- jika berhasil, tulis ke file keberhasilan,
- jika tidak, tulis ke file kesalahan.
Langkah 2. Secara manual memproses kesalahan
Sekarang kita memiliki nama-nama semua jenis yang digunakan pyContracts (mereka didefinisikan dengan dekorator new_contract), dan kita memiliki semua impor yang diperlukan, kita dapat menulis kode untuk refactoring. Ketika saya menerjemahkan dari pyContracts untuk mengetikkan secara manual, saya menyadari apa yang saya butuhkan dari skrip:
- Ini adalah perintah yang mengambil nama modul sebagai argumen (beberapa dapat digunakan), di mana sintaks penjelasan fungsi harus diganti.
- Bacalah semua file modul, bacalah. Untuk setiap file:
- jika tidak ada substring "@ kontrak", lewati file ini;
- jika demikian, ubah kode menjadi ast (pohon sintaksis abstrak);
- temukan semua fungsi yang ada di bawah dekorator kontrak untuk masing-masing:
- dapatkan dockstring, parse, lalu hapus,
- buat kamus formulir {arg_name: arg_type}, gunakan untuk menggantikan anotasi fungsi,
- ingat impor baru,
- tulis pohon yang dimodifikasi ke file melalui astunparse;
- tambahkan impor baru ke bagian atas file;
- ganti baris "kontrak @" dengan "tanda centang" karena lebih mudah daripada melalui ast.
Pecahkan pertanyaan "apakah nama ini sudah diimpor dalam file ini?" Saya tidak berniat dari awal: dengan masalah ini kita akan mengatasi menjalankan tambahan perpustakaan isort.
Tetapi setelah menjalankan versi pertama skrip, muncul pertanyaan yang masih harus diselesaikan. Ternyata 1) ast tidak maha kuasa, 2) astunparse lebih maha kuasa dari yang kita inginkan. Ini dimanifestasikan sebagai berikut:
- pada saat transisi ke pohon sintaks, semua komentar baris tunggal hilang dari kode;
- garis kosong juga menghilang;
- ast tidak membedakan antara fungsi dan metode kelas, kami harus menambahkan logika;
- sebaliknya, ketika pindah dari pohon ke kode, komentar multi-baris dalam tanda kutip tiga ditulis dalam komentar tanda kutip tunggal dan menempati satu baris, dan baris baru digantikan oleh \ n;
- tanda kurung yang tidak perlu muncul, misalnya jika A dan B dan C atau D menjadi jika ((A dan B dan C) atau D).
Kode yang melewati ast dan astunparse tetap berfungsi, tetapi keterbacaannya berkurang.
Kelemahan paling serius dari hal di atas adalah menghilangnya komentar satu baris (dalam kasus lain, kita tidak kehilangan apa-apa, tetapi hanya mendapatkan - tanda kurung, misalnya). Perpustakaan mengerikan berdasarkan ast, astunparse, dan tokenize janji untuk mencari tahu ini. Janji dan janji.
Sekarang baris kosong. Ada dua solusi yang mungkin:
- tokenize tahu bagaimana menentukan "bagian bicara" dari python, dan horast mengambil keuntungan darinya ketika mendapat token jenis komentar. Tetapi tokenize juga memiliki token seperti NewLine dan NL. Jadi, Anda perlu melihat bagaimana horor mengembalikan komentar, dan menyalin, mengganti jenis token.
- disarankan Anya, pengalaman dalam mengembangkan 2 bulan - Karena horast dapat mengembalikan komentar, pertama-tama kami mengganti semua baris kosong dengan komentar tertentu, kemudian melewati horast dan mengganti komentar kami dengan baris kosong.
- datang dengan Eugene, pengalaman dalam mengembangkan 8 tahun
Saya akan mengatakan sedikit lebih rendah tentang tanda kutip tiga dalam komentar, dan itu cukup mudah untuk memasang tanda kurung tambahan, terutama karena beberapa dari mereka dihapus oleh pemformatan otomatis.
Dalam horast kami menggunakan dua fungsi: parse dan unparse, tetapi keduanya tidak ideal - parse mengandung kesalahan internal yang aneh, dalam kasus yang jarang terjadi tidak dapat mengurai kode sumber, dan unparse tidak dapat menulis sesuatu yang memiliki tipe ketik (jenis yang Ternyata jika Anda mengetik (any_other_type)).
Saya memutuskan untuk tidak berurusan dengan parse, karena logika kerja agak membingungkan, dan pengecualian jarang terjadi - prinsip non-universalitas bekerja di sini.
Tetapi pekerjaan yang tidak jelas sangat jelas dan cukup elegan. Fungsi unparse membuat turunan dari kelas Unparser, yang init memproses pohon dan kemudian menulisnya ke file. Horast.Unparser berturut-turut diwarisi dari banyak Unparser lainnya, di mana kelas paling dasar adalah astunparse.Unparser. Semua kelas turunan hanya memperluas fungsionalitas kelas dasar, tetapi logika pekerjaan tetap sama, jadi pertimbangkan astunparse.Unparser. Ini memiliki lima metode penting:
- tulis - hanya menulis sesuatu ke file.
- fill - uses write berdasarkan jumlah indentasi (jumlah indentasi disimpan sebagai bidang kelas).
- enter - meningkatkan indentasi.
- meninggalkan - mengurangi indentasi.
- dispatch - menentukan jenis simpul pohon (katakanlah T), memanggil metode yang sesuai dengan nama jenis simpul, tetapi dengan garis bawah (mis. _T). Ini adalah metode meta.
Semua metode lain adalah metode bentuk _T, misalnya, _Module atau _Str. Dalam setiap metode seperti itu, dapat: 1) mengirimkan secara rekursif untuk node subtree, atau 2) menggunakan menulis untuk menulis isi node atau menambahkan karakter dan kata kunci sehingga hasilnya adalah ekspresi yang valid dalam python.
Sebagai contoh, kami menemukan sebuah simpul bertipe arg, di mana ia menyimpan nama argumen dan simpul anotasi. Kemudian pengiriman akan memanggil metode _arg, yang pertama-tama akan menuliskan nama argumen, kemudian menulis titik dua dan menjalankan pengiriman untuk node anotasi, di mana subtree penjelasan akan diuraikan, dan pengiriman dan menulis masih akan dipanggil untuk subtree ini.
Mari kita kembali ke masalah kita tentang ketidakmungkinan jenis tipe pemrosesan. Sekarang Anda memahami cara kerja yang tidak umum, membuat jenis Anda mudah. Mari kita buat beberapa jenis:
class NewType(object): def __init__ (self, t): self.s = ts
Ini menyimpan string dalam dirinya sendiri, dan bukan hanya seperti itu: kita perlu mengetikkan argumen fungsi, dan kita mendapatkan jenis argumen dalam bentuk string dari docking. Oleh karena itu, mari kita ganti anotasi argumen tidak dengan tipe yang kita butuhkan, tetapi dengan objek NewType yang hanya menyimpan nama tipe yang diinginkan di dalamnya.
Untuk melakukan ini, perluas horast.Unparser - tulis UnparserWithType Anda, warisan dari horast.Unparser, dan tambahkan pemrosesan jenis baru kami.
class UnparserWithType(horast.Unparser): def _NewType (self, t): self.write(ts)
Ini menggabungkan dengan semangat perpustakaan. Nama-nama variabel dibuat dalam gaya ast, dan itulah sebabnya mereka terdiri dari satu huruf, dan bukan karena saya tidak bisa memikirkan nama. Saya pikir t adalah kependekan dari tree, dan s untuk string. Omong-omong, NewType bukan string. Jika kita ingin itu ditafsirkan sebagai tipe string, maka kita harus menulis tanda kutip sebelum dan sesudah panggilan tulis.
Dan sekarang keajaiban patch monyet: ganti horast.Unparser dengan UnparserWithType kami.
Cara kerjanya sekarang: kita memiliki pohon sintaksis, ia memiliki beberapa fungsi, fungsi memiliki argumen, argumen memiliki jenis anotasi, jarum disembunyikan dalam jenis anotasi, dan kematian Koshcheev tersembunyi di dalamnya. Sebelumnya, tidak ada simpul anotasi sama sekali, kami membuatnya, dan simpul semacam itu adalah turunan dari NewType. Kami memanggil fungsi unparse untuk pohon kami, dan untuk setiap node itu disebut pengiriman, yang mengklasifikasikan simpul itu dan memanggil fungsi yang sesuai. Segera setelah fungsi pengiriman menerima simpul argumen, ia menulis nama argumen, kemudian melihat apakah ada anotasi (dulu tidak ada, tetapi kami menempatkan NewType di sana), jika ya, ia menulis titik dua dan memanggil pengiriman untuk anotasi, yang memanggil _NewType kami, yang hanya menulis string yang disimpannya - ini adalah nama tipe. Hasilnya, kita mendapatkan argumen tertulis: type.
Sebenarnya, ini tidak sepenuhnya legal. Dari sudut pandang kompiler, kami menuliskan anotasi argumen dengan beberapa kata yang tidak didefinisikan di mana pun, jadi ketika unparse menyelesaikan pekerjaannya, kami mendapatkan kode yang salah: kami membutuhkan impor. Saya cukup membentuk garis format yang benar dan menambahkannya ke awal file, dan kemudian menambahkan hasilnya ke unparse, meskipun saya bisa menambahkan impor sebagai node ke pohon sintaks, karena ast mendukung Impor dan ImportFrom node.
Memecahkan masalah tanda kutip tiga tidak lebih sulit daripada menambahkan tipe baru. Kami akan membuat kelas StrType dan metode _StrType. Metode ini tidak berbeda dengan metode _NewType yang digunakan untuk membubuhi keterangan tipe, tetapi definisi kelas telah berubah: kita tidak hanya akan menyimpan string itu sendiri, tetapi juga level tab di mana ia harus ditulis. Jumlah lekukan didefinisikan sebagai berikut: jika baris ini ditemukan dalam suatu fungsi, maka satu, jika dalam suatu metode, maka dua, dan tidak ada kasus ketika fungsi tersebut didefinisikan dalam tubuh fungsi lain dan didekorasi pada saat yang sama, dalam proyek kami.
class StrType(object): def __init__ (self, s, indent): self.s = s self.indent = indent def __repr__ (self): return '"""\n' + self.s + '\n' + ' ' * 4 * self.indent + '"""\n'
Dalam repr kita mendefinisikan seperti apa garis kita seharusnya. Saya pikir ini jauh dari satu-satunya solusi, tetapi berhasil. Seseorang dapat bereksperimen dengan astunparse.fill dan indunparse.Unparser.indent, maka itu akan menjadi lebih universal, tetapi ide ini muncul di benak saya pada saat menulis artikel ini.
Kesulitan yang diselesaikan ini berakhir. Setelah menjalankan skrip saya, masalah impor siklik terkadang muncul, tetapi ini adalah masalah arsitektur. Saya tidak menemukan solusi pihak ketiga yang siap pakai, dan untuk menangani kasus-kasus seperti itu dalam kerangka skrip saya tampaknya merupakan komplikasi serius dari tugas tersebut. Mungkin, dengan bantuan ast, dimungkinkan untuk mendeteksi dan menyelesaikan impor siklik, tetapi gagasan ini perlu dipertimbangkan secara terpisah. Secara umum, jumlah insiden semacam itu yang dapat diabaikan dalam proyek kami sepenuhnya memungkinkan saya untuk tidak memprosesnya secara otomatis.
Kesulitan lain yang saya temui adalah kurangnya pemrosesan ekspresi ast dari impor astro karena pembaca yang teliti telah mengetahui bahwa patch monyet adalah obat untuk semua penyakit. Biarkan ini menjadi pekerjaan rumahnya untuknya, tetapi saya memutuskan untuk melakukan ini: tambahkan saja impor tersebut ke file pemetaan, karena konstruksi ini biasanya digunakan untuk memotong konflik nama, dan kami memiliki beberapa di antaranya.
Terlepas dari ketidaksempurnaan yang ditemukan, skrip melakukan apa yang seharusnya dilakukan. Apa hasilnya:
- Waktu peluncuran proyek telah berkurang dari 10 menjadi 3 detik;
- Jumlah file telah berkurang karena penghapusan definisi new_contract. File-file itu sendiri berkurang: Saya tidak mengukur, tetapi rata-rata git berjumlah n baris yang ditambahkan dan yang dihapus 2n;
- IDE cerdas mulai membuat isyarat yang berbeda, karena sekarang itu bukan komentar, tetapi impor jujur;
- Keterbacaan telah meningkat;
- Kurung di suatu tempat muncul.
Terima kasih
Tautan yang bermanfaat:
- Ast
- Mengerikan
- Semua jenis node ast dan apa yang disimpan di dalamnya
- Indah menunjukkan pohon sintaksis
- Isort