Build spy fmt formatter#496
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new spy fmt CLI subcommand intended to format .spy sources by preprocessing SPy-specific syntax (var/const) into Python-compatible code, running ruff format, and reinserting SPy-specific tokens afterward.
Changes:
- Introduces
spy fmtcommand wiring and a newSPyFormatterimplementation that shells out toruff format. - Extends
magic_py_parse.pywith token-tracking helpers to reinsertvar/constafter formatting. - Adds tests for token tracking/reinsertion and a CLI-level formatting test.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
spy/tests/test_fmt.py |
New tests for SPy-specific token tracking and reinsertion logic. |
spy/tests/test_cli.py |
Adds an integration test for spy fmt formatting a file. |
spy/magic_py_parse.py |
Adds scope/occurrence-based tracking + reinsertion of var/const tokens. |
spy/cli/commands/fmt.py |
New fmt subcommand implementation. |
spy/cli/cli.py |
Registers the new fmt subcommand. |
spy/analyze/fmt.py |
Implements Ruff-based formatting and SPy token reinsertion pipeline. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
JeffersGlass
left a comment
There was a problem hiding this comment.
HI @kanin-kearpimy! This is really cool! A few little comments dropped in.
My biggest though is that I think this feature should be called format, not fmt, and just have fmt be an alias in the CLI. I think that will make it more clear across the code what the intent is. So, fmt.py -> format.py, test_fmt.py -> test_format.py, etc.
ddf1b3d to
0d5679d
Compare
antocuni
left a comment
There was a problem hiding this comment.
@kanin-kearpimy sorry for taking so long to review this, the last month has been crazy.
So, first of all: congratulations for having invented this way to solve the problem. It's very clever and I didn't think about it.
That said, I think it's too much clever and I am a bit worried about the extra complexity added to magic_py_parse.
While reviewing this, I got a different idea which would make spy fmt much easier to implement. I prototyped this idea in #540.
Basically, instead of "stripping" var/const and keeping track of varkind_locs in a separate table, we can replace var x with a single identifier var·x, using the special char MIDDLE DOT.
If we have that, spy fmt would become as easy as:
- use magic_py_parse to produce the "pythonized" source
- use a regex to replace
var·····xwithvar·x - run
ruff fmton it - use a regex to replace
var·xback tovar x
The nice part is thatlen('var x') == len('var·x'), soruff fmtshould automatically do the right thing w.r.t. e.g. long lines.
What do you think?
8257407 to
464c9d1
Compare
|
Hi, I refactored code to use new |
JeffersGlass
left a comment
There was a problem hiding this comment.
This might be a nit, but I would love to see the filenames and function names all changed from fmt to format. (fmt.py -> format.py, test_fmt.py -> test_format.py, etc.). There's nothing really gained by the abbreviation here and it would be nice to be consistent with the rest of the command filenames and functions.
Other than that, LGTM!
antocuni
left a comment
There was a problem hiding this comment.
thanks @kanin-kearpimy .
This is much easier and simpler than the previous approach, good job :).
I left some comments: many are style issues and nitpicks, but two are real:
spy formatshould operate on filenames, not modnames- it should raise
SPyErrorfor messages which are meant to be shown to the user, notRuntimeError.
| check=True, | ||
| capture_output=True, | ||
| text=True, | ||
| cwd=repo_root, |
There was a problem hiding this comment.
why do you need cwd=repo_root here?
There was a problem hiding this comment.
It's for config in pyproject.toml. However, I adjusted it a bit to explicitly adding config file on command.
cmd = ["ruff", "format", tmp_path]
if os.path.exists(config_path):
cmd.extend(["--config", config_path])
result = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
)
|
|
||
| async def fmt(args: Fmt_Args) -> None: | ||
| """Format SPy file""" | ||
| # TODO: implement directory formatting. |
There was a problem hiding this comment.
it would be nice to have support for
- directoris, e.g.
spy format /tmp/somedir) - multiple files, e.g.
spy format /tmp/*.spy)
But I think it's fine to do this in a follow-up PR :)
There was a problem hiding this comment.
Of course. Let me share new PR with you soon.
What do you mean here sorry? @antocuni |
the original comment asked to return |
|
Side remark: it would be very convenient if |
that's an interesting idea. Not 100% it would make the code more readable though, because redshifted code tends to have very long identifiers. Maybe we can make it working by using a longer line length than usual? |
d509e33 to
2c82502
Compare
|
|
Hello @antocuni , most of comments has been resolved. For multiple files / dictionary, New PR is coming. |
Issue: #457
Description
Hello, this PR introduce SPy formatter via
spy fmtcommand. SPy actually extends Python syntax with extra language grammar. Technically, we can use Python formatter. However, we need to handle SPy-specificconstandvardeclaration.spy formatis calling:.spycode to `python.preprocess, we mark SPy grammar (e.g.var x,const ywith MIDDLE DOT likevar·xandconst·y.var·xandconst·ytovar xandconst yNote
This PR cover only single file formatter. Adding multiple files or directory are not that hard. To concentrate only on SPy formatter, I propose we leave those features for another issue (I'll create it later). Such issue is also good for first time contributor.
DEPRECATED DO NOT READ
spy fmtis calling:.spyfile content and tokenize.constandvar<< (Please see below for more detail)constandvarout.ruffformatter.SPy-specific declaration: Scoping Tracker
We scope source code by function, class, and some control-flow branch such as
if,else,while,for. Tracker push/pop those scope into stack. These scopes are not affected byruffformatter. So, we can track nth-variable by each scope.In picture above, we have two scope:
module_def-mainandmodule_def-main_if-1. So, dictionary tracker hasmodule_def-main__x-1module_def-main_if-1__y-1If we found SPy-specific like
const,var, we marked them in dictionary. When we perform step (4) to reintroduce SPy-specific, we perform scope tracker against step (1) dictionary.Please note we also track occurrence of variables and keywords (
if,else,while,for) against its scope.