2015. február 8., vasárnap

Javascript függvény paraméterének kiolvasása PyV8-al

(Ez a bejegyzés egy kicsit módosítva lett 2015. február 9-én)

Rendbe, most hogy ilyen szépen elkészítettük a saját PyV8 példányunkat, ideje lenne kipróbálni valami kóddal, hogy működik-e rendesen.

A saját projektjeim közül hoztam a mostani témát: egy weboldalon egy Javascript Object értékét szeretném megkapni és átalakítani dict-é. A mostani történetben a csavar az, hogy ez az objektum egy függvény paramétere, ami ráadásul a jQuery ready() metódusába van belenyomogatva. Tehát nem holmi pi=3.14 változót kellene kipiszkálni.

(A projektről homályosan: Van egy weboldal, amin egy Google Maps-es térkép látható. Erre a térképre valamilyen saját library-vel markereket tesznek. Én egy adott, sokszög alapú területen levő markereket akarom kigyűjteni. A probléma megoldásának első részével foglalkozik a mai bejegyzés: a weboldal térképén látható összes koordináta összegyűjtésével. Egy későbbi részben majd kitaláljuk, hogy hogyan lehet kiválogatni azokat, amik egy területen belül vannak.)

Lássuk a kódot

Ebben a kis részletben csak a lényeg látszódik. Azzal hogy hogy halásztam ki a DOM-ból most nem foglalkozok (amúgy lxml-el):

$(document).ready(function(){
    new AwesomeLibrary.PutMarkers('#map', {
        data: [{id: '1', lat: '47.160971', lng: '16.4878386'}]
    });
});

Maga a forráskód nagyon egyértelmű, a PutMarkers() második paraméterének az értékére van szükség ({data:[{id: 1, lat: '47.160971', lng: '16.4878386'}]})

Megoldás

A jó multkor egyszerűbbnek találtam, ha először egy fake Javascript kódot eval()-olok a contextben, amivel módosítottam a később ténylegesen meghívott metódusok működését. Ahhoz hogy ez most is működjön kézzel deklarálnom kell a $, document (mivel itt nincs DOM), ready(), AwesomeLibrary, PutMarkers értékeit, hogy ne kapjak sehol se undefined is not a function errort. Így szépen végig fut az egész kóceráj, de a sajnos a PutMarkers visszatérési értékét az eredeti függvény nem teszi ki globális változónak, és maga az anonim függvény se tér vissza semmivel se (főleg nem a koordinátákkal). Jobb ötlet híján a saját PutMarkers implementációmban kiraktam globális változóba a kapott paramétereket, de aztán inkább úgy döntöttem, hogy kipróbálok valami mást, és minden hiányzó függvényt egy Python osztályból rakosgatok a contextbe.

class Globals(PyV8.JSClass):
    def __init__(self):
        super(Globals, self).__init__()
        self.coords = []

    def PutMarkers(self, selector, coords):
        coords = PyV8.convert(coords)

        for coord in coords['data']:
            self.coords.append({
                'lat': float(coord['lat']),
                'lng': float(coord['lng']),
                'id': int(coord['id'])
            })

    def ready(self, function):
        function()

    def __call__(self, *args, **kwargs):
        return self

    def __getattr__(self, *args):
        return self

Az osztály legfontosabb metódusa a __getattr__(). Ez minden olyan esetben meghívódik, ha a Python nem találja az objektum egy property-jét. Mivel ez a metódus a globális névtérbe került, ezért minden olyan esetbe, amikor a V8 a globális névtérből keres egy változót, ez a Python metódus fog lefutni. Ide kerül minden olyan logika ami azokat az eseteket kezeli le, amikor valami deklarálatlan értéket keres a V8, de egyáltalán nem fontos, hogy mi annak az értéke. Másik fontos metódus a __call__(). Ennek segítségével a Globals példányunk függvényekhez hasonlóan tud viselkedni (meg lehet hívni).

A két magic method kombinációjával meg lehet oldani, hogy a számunkra nem érdekes változókat és metódusokat ne kelljen kézzel deklarálgatni. Amikor a V8 egy property-t keres, akkor szépen megkapja a objektumunkat a __getattr__()-ből. Ha abban a property-ben még egy propertyt keres, akkor megint csak megkapja az objektumot, de ha a proprty értékét callable-nek tekinti az is működni fog, mivel az objektum függvényként is tud viselkedni.

Magyarul tehát a Javascript kódban bármikor is egy deklarálatlan változót talál a V8, akkor azt mindig a Globals objektum attribútumai között keresi, és bármikor is talál egy deklarálatlan függvényt, meg fogja tudni hívni, mert a Globals példánya callable. Gyakorlatilag ez egy lánc (method chaining), így mindig minden keresett érték deklarált lesz. (Nem kerülünk rekurzióba, mivel a property-k kibontogatása véges).

Ha ez megvan, akkor jöhetnek az “érdekes metódusok”, szóval azok, amik ténylegesen végeznek valami munkát. A Javascript forráskódban ezek a ready() és PutMarkers().

A $ mindegy hogy mit csinál, csak függvény legyen, és valami olyan objektummal térjen vissza, amiben létezik a ready() metódus. A __getattr__() visszatér egy objektummal, ami függvényként viselkedik, és függvény által visszakapott objektumban létezik a ready() metódus (lásd kicsit lejjebb).
A document, AwesomeLibrary értékei hasonlóan. Egy csomót spóroltunk!

A ready() metódus a Globals osztályban magkapja az anonim függvény, egy JSFunction Python objektumként. Ezt ugyan úgy meg lehet hívni mint bármelyik sima függvényt, de természetesen a kódot a V8 futtatja, a kontextusban.

A történet vége a PutMarkers() metódus. Ez 2 paramétert vár, egy css selectort, amivel nem csinálunk semmit, és magát az értékes adatot, szóval a koordinátákat. Innen már nagyon egyszerű a dolog, a PyV8.convert() átalakítja dict-é az Javascript Object-et (így kikerül a context-ből az adat), majd egy sima iterációval bepakolgatjuk az összes koordinátát a self.coords tömbbe.

script_content = """
$(document).ready(function(){
    new AwesomeLibrary.PutMarkers('#map', {
        data: [{id: '1', lat: '47.160971', lng: '16.4878386'}]
    });
});"""

context_globals = Globals()
with PyV8.JSContext(context_globals) as ctx:
    ctx.eval(script_content)

print(context_globals.coords)

Példányosítjuk a Globals osztályt, majd nyitunk egy JSContext-et. Paraméternek átadjuk a csinos objektumunkat. A contextben pedig futtatjuk a Javascript kódot (script_content).

A kontextuson kívül a context_globals.coords propertyben már ott is vannak a koordináták

>>> print(context_globals.coords)
[{'lat': 47.160971, 'id': 1, 'lng': 16.4878386}]

Zárás

Azt hiszem jól látszódik, hogy kivállóan együtt tudnak működni a Python-os osztályok és a V8. Lényegesen elegánsabb ez a megoldás, mint kézzel kikapuzni egy másik Javascript fájlban az összes előforduló de haszontalan változót és aztán betölteni azt a context elején.

A teljes kód megtekinthető ezen a Gist-en.

A következő részben kitaláljuk, hogy hogy lehet kiválogatni az egy területen levő koordinátákat.

-slp