Skip to content

Commit 4165020

Browse files
Copilotyarikoptic
andcommitted
Add migrate command with migration framework and initial migrations
- Add migrate.py module with base migration framework including Migration class, MigrationRegistry, and decorators - Add migrations.py with three specific migrations: - standardize_generatedby: Convert pre-standard provenance to GeneratedBy (BEP028) - fix_inheritance_overloading: Check for deprecated inheritance overloading patterns (PR #1834) - fix_tsv_entity_prefix: Check for missing entity prefixes in TSV files (PR #2281) - Add CLI commands: 'bst migrate list', 'bst migrate run', 'bst migrate all' - Add comprehensive tests for migration framework and specific migrations - All tests passing (24 tests total) Co-authored-by: yarikoptic <[email protected]>
1 parent 5c23a0c commit 4165020

File tree

5 files changed

+1249
-0
lines changed

5 files changed

+1249
-0
lines changed

tools/schemacode/src/bidsschematools/__main__.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import sys
66
from itertools import chain
7+
from pathlib import Path
78

89
import click
910

@@ -180,5 +181,153 @@ def pre_receive_hook(schema, input_, output):
180181
sys.exit(rc)
181182

182183

184+
@cli.group()
185+
def migrate():
186+
"""Migrate BIDS datasets to adopt standardized conventions"""
187+
pass
188+
189+
190+
@migrate.command("list")
191+
def migrate_list():
192+
"""List all available migrations"""
193+
from .migrate import registry
194+
from . import migrations # noqa: F401 - Import to register migrations
195+
196+
migrations_list = registry.list_migrations()
197+
198+
if not migrations_list:
199+
lgr.info("No migrations available")
200+
return
201+
202+
click.echo("Available migrations:\n")
203+
for mig in sorted(migrations_list, key=lambda x: x["version"]):
204+
click.echo(f" {mig['name']} (version {mig['version']})")
205+
click.echo(f" {mig['description']}\n")
206+
207+
208+
@migrate.command("run")
209+
@click.argument("migration_name")
210+
@click.argument("dataset_path", type=click.Path(exists=True))
211+
@click.option(
212+
"--dry-run",
213+
is_flag=True,
214+
help="Show what would be changed without modifying files",
215+
)
216+
def migrate_run(migration_name, dataset_path, dry_run):
217+
"""Run a specific migration on a BIDS dataset
218+
219+
MIGRATION_NAME is the name of the migration to run (use 'migrate list' to see available)
220+
221+
DATASET_PATH is the path to the BIDS dataset root directory
222+
"""
223+
from .migrate import registry
224+
from . import migrations # noqa: F401 - Import to register migrations
225+
226+
dataset_path = Path(dataset_path).resolve()
227+
228+
if not (dataset_path / "dataset_description.json").exists():
229+
lgr.error(
230+
f"No dataset_description.json found in {dataset_path}. "
231+
"Is this a valid BIDS dataset?"
232+
)
233+
sys.exit(1)
234+
235+
try:
236+
result = registry.run(migration_name, dataset_path, dry_run=dry_run)
237+
except ValueError as e:
238+
lgr.error(str(e))
239+
lgr.info("Use 'bst migrate list' to see available migrations")
240+
sys.exit(1)
241+
242+
# Display results
243+
if result.get("modified_files"):
244+
click.echo(f"\nModified files ({len(result['modified_files'])}):")
245+
for filepath in result["modified_files"]:
246+
click.echo(f" - {filepath}")
247+
248+
if result.get("warnings"):
249+
click.echo(f"\nWarnings ({len(result['warnings'])}):")
250+
for warning in result["warnings"]:
251+
click.echo(f" - {warning}")
252+
253+
if result.get("suggestions"):
254+
click.echo(f"\nSuggestions ({len(result['suggestions'])}):")
255+
for suggestion in result["suggestions"]:
256+
click.echo(f" - {suggestion}")
257+
258+
click.echo(f"\n{result['message']}")
259+
260+
if not result["success"]:
261+
sys.exit(1)
262+
263+
264+
@migrate.command("all")
265+
@click.argument("dataset_path", type=click.Path(exists=True))
266+
@click.option(
267+
"--dry-run",
268+
is_flag=True,
269+
help="Show what would be changed without modifying files",
270+
)
271+
@click.option(
272+
"--skip",
273+
multiple=True,
274+
help="Skip specific migrations (can be used multiple times)",
275+
)
276+
def migrate_all(dataset_path, dry_run, skip):
277+
"""Run all available migrations on a BIDS dataset
278+
279+
DATASET_PATH is the path to the BIDS dataset root directory
280+
"""
281+
from .migrate import registry
282+
from . import migrations # noqa: F401 - Import to register migrations
283+
284+
dataset_path = Path(dataset_path).resolve()
285+
286+
if not (dataset_path / "dataset_description.json").exists():
287+
lgr.error(
288+
f"No dataset_description.json found in {dataset_path}. "
289+
"Is this a valid BIDS dataset?"
290+
)
291+
sys.exit(1)
292+
293+
migrations_list = registry.list_migrations()
294+
skip_set = set(skip)
295+
296+
if not migrations_list:
297+
click.echo("No migrations available")
298+
return
299+
300+
click.echo(f"Running {len(migrations_list)} migration(s) on {dataset_path}")
301+
if dry_run:
302+
click.echo("DRY RUN: No files will be modified\n")
303+
304+
results = []
305+
for mig in sorted(migrations_list, key=lambda x: x["version"]):
306+
if mig["name"] in skip_set:
307+
click.echo(f"Skipping: {mig['name']}")
308+
continue
309+
310+
click.echo(f"\nRunning: {mig['name']} (version {mig['version']})")
311+
result = registry.run(mig["name"], dataset_path, dry_run=dry_run)
312+
results.append((mig["name"], result))
313+
314+
if result.get("modified_files"):
315+
click.echo(f" Modified {len(result['modified_files'])} file(s)")
316+
if result.get("warnings"):
317+
click.echo(f" {len(result['warnings'])} warning(s)")
318+
if result.get("suggestions"):
319+
click.echo(f" {len(result['suggestions'])} suggestion(s)")
320+
click.echo(f" {result['message']}")
321+
322+
# Summary
323+
click.echo("\n" + "=" * 60)
324+
click.echo("Migration Summary:")
325+
click.echo("=" * 60)
326+
327+
for name, result in results:
328+
status = "✓" if result["success"] else "✗"
329+
click.echo(f"{status} {name}: {result['message']}")
330+
331+
183332
if __name__ == "__main__":
184333
cli()

0 commit comments

Comments
 (0)