Skip to content

Generate the vgplot-python API using Python #1154

Description

@dangotbanned

Why?

Why should we do it?

IMO, a strong point of altair is that because API generation takes place in the language of
it's users - the bar is relatively low for new contributors.
For example, there are 12 authors for the most recent 30 commits to vega/altair/tools

Making this change would lower the barrier to who can maintain this code 1 and hopefully
increase the chances of a bug reporter being able to fix their issue themselves.

Surely LLMs can bridge the gap for those unfamiliar with JS?

Aha, got you!

If a potential contributor doesn't know JS, are they in a position to judge the correctness of LLM
output in that language?

Why did I already do it?

Note

Everything from here onwards is explaining my rabbit hole(s)
Feel free to stop reading now!

A couple of weeks ago I was looking into fixing (#1095 (comment)) as part of (#1088).
I'm embarrassed to admit it, but it took me quite some time to wrap my head around generate-python-api.js.
The lack of typing made the process pretty painful, as my IDE wasn't able to fill in the gaps.

To get myself out of that pickle, I added some JSDoc 2 and then began porting the rest in python to figure out what I needed to change.

Note

This first rabbit hole led to a second one - which sidetracked me for over a week -
before continuing on this one a couple days ago (2d87f54a6cf435e47c1ea600b48124c7a6873cf8)

So anyway, back to rabbit hole 1!

Equipped with my recent knowledge of mosaic-schema.json - the rest fell out without too much of battle 😄

See changes (compare/main...dangotbanned:mosaic:vgplot-python/fix-dunder-all-2)

I was able to fix __all__!

Findings

I've found some other oddities in the process, which I wanted to mention before opening a PR 3

TL;DR: (1) I unintentionally fixed; (2) I haven't repeated but still can; (3) I have repeated but would prefer not to.

1 - type -> type_?

This code is responsible for renaming type:

/** Python identifier for a schema (camelCase) name, keyword-safe. */
function ident(name) {
const s = camelCaseToSnake(name);
return PYTHON_KEYWORDS.has(s) ? s + '_' : s;
}

Which appears here:

type_: ChannelValue | UNSET = UNSET,

Despite it already having been documented that this is not needed, here:

// Keywords that cannot appear as a bare keyword-argument name. `type` is a
// builtin, not a reserved word, so it stays valid as a kwarg.
const PYTHON_KWARG_UNSAFE = new Set(
[...PYTHON_KEYWORDS].filter(k => k !== 'type')
);

I only noticed this because I used keyword.iskeyword 4, which correctly allows type as a
keyword, but that also made me realize there is no coverage for either 😬

2 - transform-keys.js

This one confused me, because it lists every transform as they all have "a generated vgplot Python
API function."

// Shared artifact for ast-to-python.js: the transform keys that have a
// generated Python function, so the spec emitter needs no hand-kept list.
writeFileSync(resolve(SPEC_GEN_DIR, 'transform-keys.js'),
'// DO NOT EDIT. Generated from the Mosaic JSON schema by bin/generate-python-api.js.\n' +
'// Regenerate with: pnpm run generate:python-api\n\n' +
'/** Transform keys with a generated vgplot Python API function. */\n' +
'export const TRANSFORM_KEYS = new Set([\n' +
transforms.map(({ key }) => ` '${key}',`).join('\n') +
'\n]);\n');

To figure out if it did anything at all, I deleted this line where TRANSFORM_KEYS is used:

Show me the diff

diff --git a/docs/public/specs/python/wnba-shots.py b/docs/public/specs/python/wnba-shots.py
index abd185a6..d1f74657 100644
--- a/docs/public/specs/python/wnba-shots.py
+++ b/docs/public/specs/python/wnba-shots.py
@@ -32,7 +32,7 @@ view = vg.vconcat(
             y="y_position",
             fill=vg.avg("score_value"),
             r=vg.count(),
-            tip={"format": {"x": False, "y": False}},
+            tip=vg.format({"x": False, "y": False}),
         ),
         vg.line(court, stroke_linecap="butt", stroke_opacity=0.5, x="x", y="y", z="z"),
         vg.name("shot-chart"),
diff --git a/packages/vgplot/spec/src/ast-to-python.js b/packages/vgplot/spec/src/ast-to-python.js
index 345992de..546beebb 100644
--- a/packages/vgplot/spec/src/ast-to-python.js
+++ b/packages/vgplot/spec/src/ast-to-python.js
@@ -207,7 +207,6 @@ function emitEncoding(v, ctx) {
   if (!keys.length) return literal(v, 0, ctx);
   const key = keys[0];
   if (key === 'sql' && keys.length === 1) return `vg.sql(${literal(v.sql, 0, ctx)})`;
-  if (!TRANSFORM_KEYS.has(key)) return literal(v, 0, ctx);
   const { [key]: val, ...opts } = v;
   const args = (val === '' || val == null) ? []
     : (Array.isArray(val) ? val : [val]).map(x => literal(x, 0, ctx));
diff --git a/specs/python/wnba-shots.py b/specs/python/wnba-shots.py
index abd185a6..d1f74657 100644
--- a/specs/python/wnba-shots.py
+++ b/specs/python/wnba-shots.py
@@ -32,7 +32,7 @@ view = vg.vconcat(
             y="y_position",
             fill=vg.avg("score_value"),
             r=vg.count(),
-            tip={"format": {"x": False, "y": False}},
+            tip=vg.format({"x": False, "y": False}),
         ),
         vg.line(court, stroke_linecap="butt", stroke_opacity=0.5, x="x", y="y", z="z"),
         vg.name("shot-chart"),

I'm very confused by this solution. Nothing about the code
suggests that this is what's happening. Does this make sense to anyone else?
My questions here are:

  1. Why isn't there a vg.format?
    1. If there was, then TRANSFORM_KEYS would be a noop
  2. Can we drop this part of the API generation and handle whatever this is doing in a clearer
    way?
    1. Either by adding the missing format helper
    2. Or identifying when {"format": {... should be generated by using something named FORMAT*

3 - Docstrings

I hope it isn't too controversial to say that this is a tough read 😉

/** First sentence of a schema description, with markdown links stripped and
* escaped for a docstring. */
function docline(desc, fallback) {
let text = (desc || fallback || '')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // [text](url) -> text
.replace(/\[([^\]]+)\]\[[^\]]*\]/g, '$1') // [text][ref] -> text
.replace(/\[(\d+)\]/g, '') // bare footnote [1] -> (removed)
.replace(/\[([^\]]+)\]/g, '$1') // [text] shortcut -> text
.replace(/\s+/g, ' ').trim();
const first = text.split(/(?<=\.)\s/)[0] || fallback;
return first.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
}

But the larger issue to me is I don't understand why this is happening in the first place.

The docs in vgplot/_generated/attributes.py could be so much better if they just kept things
as they are in the original (PlotAttribute.ts)

How do I know? Because here's the docs and typing sitting pretty in python!

If you made it this far

Apologies for the essay! 😅
This one leads into why packages/vgplot/spec-python even exists - which I need to get around to writing more about ⏲️

Footnotes

  1. understanding 1 language vs 2

  2. which I removed in 7e1ab15013e38e2d42c3cb14045dcf535649247a

  3. if there's interest

  4. Python stdlib function

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonPull requests that update Python code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions