File size: 54,574 Bytes
9a35cd9
537aef6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a35cd9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Flit Migration guide — map setup files to flit config</title>
<meta name="description" content="This free browser tool maps setup.py, setup.cfg, and older project configuration to replacement Flit settings and shows build blockers.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/flit-migration-check"><style data-yj-mfit="1">#out{max-width:100%}#out td,#out th{overflow-wrap:anywhere}#out pre,#out code{white-space:pre-wrap;overflow-wrap:anywhere}@media (max-width:640px){table{display:block;max-width:100%;overflow-x:auto}pre{max-width:100%;overflow-x:auto}code,a{overflow-wrap:anywhere}img,svg,video,canvas,iframe{max-width:100%}textarea,input,select{max-width:100%;box-sizing:border-box}.yj-lgbar{position:static!important;justify-content:flex-end;margin:0 0 10px}h1,h2,h3,h4,p,li,dd,dt,blockquote,figcaption,summary,label,.res,.lead{overflow-wrap:anywhere}label{max-width:100%;box-sizing:border-box}#out table:not(:has(tr>:nth-child(4))){display:table;table-layout:fixed;width:100%}#out table:has(tr>:nth-child(4)),#out table:has(tr>:nth-child(4)) tbody,#out table:has(tr>:nth-child(4)) tr,#out table:has(tr>:nth-child(4)) td{display:block;width:auto;overflow:visible}#out table:has(tr>:nth-child(4)) thead{display:none}#out table:has(tr>:nth-child(4)) tr{padding:10px 0;border-bottom:1px solid rgba(128,128,128,.28)}#out table:has(tr>:nth-child(4)) td{border:0;padding:1px 0}#out table:has(tr>:nth-child(4)) td:nth-child(-n+3){display:inline-block;margin-right:10px;font-size:12px;opacity:.78}}</style>
</head>
<body>
<div class="wrap">
<h1>Flit Migration guide — map setup files to flit config</h1>
<p class="lede">Flit migration map for Python packages: finds every setup.py, setup.cfg and setuptools/old-flit pyproject.toml line and prints the exact flit_core 4 [project] line that replaces it, plus the blockers Flit cannot build. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false"># setup.py for "tidyframe" -- the file this project still builds from
import os
from setuptools import setup, find_packages

here = os.getcwd()
with open(os.path.join(here, "README.md")) as f:
    readme = f.read()

setup(
    name="tidyframe",
    version="2.4.1",
    description="Tidy helpers for pandas frames",
    long_description=readme,
    long_description_content_type="text/markdown",
    author="Dana Example",
    author_email="dana@example.org",
    url="https://github.com/example/tidyframe",
    license="MIT",
    packages=find_packages(exclude=["tests"]),
    package_dir={"": "src"},
    include_package_data=True,
    package_data={"tidyframe": ["schemas/*.json"]},
    data_files=[("share/man/man1", ["docs/tidyframe.1"])],
    python_requires="&gt;=3.8",
    setup_requires=["wheel"],
    install_requires=["pandas&gt;=2.0", "click&gt;=8"],
    extras_require={"test": ["pytest&gt;=7"], "docs": ["sphinx"]},
    entry_points={
        "console_scripts": ["tidyframe=tidyframe.cli:main"],
        "pandas_plugins": ["tidy=tidyframe.plugin:register"],
    },
    classifiers=["License :: OSI Approved :: MIT License"],
)</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Check the open setup.py, setup.cfg or pyproject.toml: every setuptools or old-flit line with its line number and the exact flit_core 4 replacement line, plus the blockers Flit cannot build. <strong>In the editor:</strong> Whole workspace: one dated migration plan mapping every item in every package to its flit_core 4 replacement, and a team CI gate that fails a build adding new setuptools-only use. — that part asks for a licence key. <a href="https://getreadystack.com/api/buy/cl/polar_cl_VI4ER6LWKbRiLZHzzqUpAK77hPReaZmifZSkj19xgaN">Workspace migration plan + team CI gate — $29 once</a></div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var FLIT_RULES = [
 {
  "id": "SP01",
  "check": "setuptools_setup_py",
  "files": [
   "setup.py"
  ],
  "re": "^\\s*(from\\s+(setuptools|distutils)[\\w.]*\\s+import|import\\s+(setuptools|distutils))",
  "sev": "warn",
  "msg": "setup.py drives this build; flit_core reads no setup.py",
  "fix": "delete setup.py; in pyproject.toml write [build-system] requires = [\"flit_core \u003e=3.11,\u003c5\"] and build-backend = \"flit_core.buildapi\"",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP02",
  "check": "compiled_extension",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bext_modules\\s*=|\\bExtension\\s*\\(|\\bcythonize\\s*\\(|\\bPybind11Extension\\b",
  "sev": "error",
  "msg": "BLOCKER: compiled extension; Flit builds pure Python packages only (\"If your package needs a build step, you won't be able to use Flit\")",
  "fix": "keep setuptools for this package, or split the pure-Python part into its own Flit package",
  "source": "https://flit.pypa.io/en/stable/rationale.html"
 },
 {
  "id": "SP03",
  "check": "custom_cmdclass",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bcmdclass\\s*=",
  "sev": "error",
  "msg": "BLOCKER: custom build command (cmdclass); flit_core runs no build hooks",
  "fix": "move the generated file into the source tree before the build, or keep setuptools",
  "source": "https://flit.pypa.io/en/stable/rationale.html"
 },
 {
  "id": "SP04",
  "check": "setup_requires",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bsetup_requires\\s*=",
  "sev": "warn",
  "msg": "setup_requires has no Flit equivalent",
  "fix": "list build tools in [build-system] requires; runtime needs go in [project] dependencies",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP05",
  "check": "install_requires",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\binstall_requires\\s*=",
  "sev": "warn",
  "msg": "install_requires",
  "fix": "[project] dependencies = [\"requests \u003e=2.6\", ...] (same specifiers and ; markers)",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP06",
  "check": "extras_require",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bextras_require\\s*=|^\\s*\\[options\\.extras_require\\]",
  "sev": "warn",
  "msg": "extras_require",
  "fix": "[project.optional-dependencies] test = [\"pytest \u003e=2.7.3\"]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP07",
  "check": "console_scripts",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "console_scripts",
  "sev": "warn",
  "msg": "console_scripts entry point",
  "fix": "[project.scripts] mycmd = \"mypkg:main\"",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP08",
  "check": "gui_scripts",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "gui_scripts",
  "sev": "warn",
  "msg": "gui_scripts entry point",
  "fix": "[project.gui-scripts] mygui = \"mypkg.gui:main\"",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP09",
  "check": "entry_points",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bentry_points\\s*=|^\\s*\\[options\\.entry_points\\]",
  "sev": "info",
  "msg": "entry_points block",
  "fix": "other groups become [project.entry-points.\"group.name\"] (quote a dotted group); console_scripts/gui_scripts go to [project.scripts]/[project.gui-scripts]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP10",
  "check": "data_files",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bdata_files\\s*=",
  "sev": "warn",
  "msg": "data_files installs files outside the package",
  "fix": "put them under data/ and add [tool.flit.external-data] directory = \"data\" (flit_core \u003e=3.7)",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP11",
  "check": "package_data",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\b(package_data|include_package_data|exclude_package_data)\\s*=|^\\s*\\[options\\.package_data\\]",
  "sev": "info",
  "msg": "package_data / include_package_data",
  "fix": "delete it: Flit includes data files inside the package folder automatically",
  "source": "https://flit.pypa.io/en/stable/rationale.html"
 },
 {
  "id": "SP12",
  "check": "package_dir",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bpackage_dir\\s*=",
  "sev": "info",
  "msg": "package_dir",
  "fix": "delete it: Flit finds the package next to pyproject.toml or in src/; if the import name differs from the PyPI name add [tool.flit.module] name = \"importname\"",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP13",
  "check": "find_packages",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bfind_(namespace_)?packages\\s*\\(|^\\s*packages\\s*=",
  "sev": "info",
  "msg": "packages / find_packages()",
  "fix": "delete it: name only the top-level import package; Flit includes its subpackages automatically",
  "source": "https://flit.pypa.io/en/stable/rationale.html"
 },
 {
  "id": "SP14",
  "check": "static_version",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "^\\s*version\\s*=\\s*(['\\\"]|[\\d])",
  "sev": "info",
  "msg": "version set in setup metadata",
  "fix": "[project] version = \"1.2.3\", or drop it and add dynamic = [\"version\"] so Flit reads {DV} from the package",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP15",
  "check": "attr_version",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "version\\s*=\\s*attr\\s*:|version\\s*=\\s*(get_version|read_version)\\s*\\(",
  "sev": "info",
  "msg": "version read from code at build time",
  "fix": "dynamic = [\"version\"]; Flit reads {DV} from the top-level module itself",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP16",
  "check": "scm_version",
  "files": [
   "setup.py",
   "setup.cfg",
   "pyproject.toml"
  ],
  "re": "use_scm_version|setuptools_scm|setuptools-scm|versioneer",
  "sev": "error",
  "msg": "BLOCKER: version from git tags (setuptools_scm/versioneer); Flit dynamic accepts only version, description, import-names, import-namespaces",
  "fix": "write the version into {DV} in the package and add dynamic = [\"version\"]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP17",
  "check": "python_requires",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bpython_requires\\s*=",
  "sev": "warn",
  "msg": "python_requires",
  "fix": "[project] requires-python = \"\u003e=3.8\" (flit_core 4 itself needs Python 3.8+ to build)",
  "source": "https://flit.pypa.io/en/stable/history.html"
 },
 {
  "id": "SP18",
  "check": "long_description",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\blong_description\\s*=|\\blong_description_content_type\\s*=",
  "sev": "warn",
  "msg": "long_description",
  "fix": "[project] readme = \"README.md\" (.md, .rst or .txt picks the content type)",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP19",
  "check": "legacy_license",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "^\\s*license(_files?)?\\s*=",
  "sev": "warn",
  "msg": "license set in setup metadata",
  "fix": "[project] license = \"MIT\" (a valid SPDX expression, flit_core \u003e=3.11) and license-files = [\"LICENSE*\"]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP20",
  "check": "script_files",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "^\\s*scripts\\s*=",
  "sev": "warn",
  "msg": "scripts= copies raw script files; Flit has no script-files field",
  "fix": "turn each script into a function and add [project.scripts] name = \"mypkg.cli:main\"",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP21",
  "check": "author_fields",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\b(author|author_email|maintainer|maintainer_email)\\s*=",
  "sev": "info",
  "msg": "author / author_email",
  "fix": "[project] authors = [{ name = \"Jane Doe\", email = \"jane@example.org\" }]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP22",
  "check": "url_fields",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\b(url|project_urls|download_url)\\s*=",
  "sev": "info",
  "msg": "url / project_urls",
  "fix": "[project.urls] Source = \"https://github.com/you/pkg\"",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP23",
  "check": "classifiers",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "\\bclassifiers\\s*=",
  "sev": "info",
  "msg": "classifiers",
  "fix": "[project] classifiers = [...] (add \"Private :: Do Not Upload\" to block an accidental PyPI upload)",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SP24",
  "check": "name_description",
  "files": [
   "setup.py",
   "setup.cfg"
  ],
  "re": "^\\s*(name|description)\\s*=",
  "sev": "info",
  "msg": "name / description",
  "fix": "[project] name = \"tidyframe\"; description = \"...\" or dynamic = [\"description\"] to take the module docstring",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "SC01",
  "check": "setup_cfg_metadata",
  "files": [
   "setup.cfg"
  ],
  "re": "^\\s*\\[(metadata|options)\\]",
  "sev": "warn",
  "msg": "setup.cfg [metadata]/[options] section",
  "fix": "move each key into the standard [project] table of pyproject.toml, then delete setup.cfg",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP01",
  "check": "non_flit_backend",
  "files": [
   "pyproject.toml"
  ],
  "re": "^\\s*build-backend\\s*=\\s*\\\"(?!flit_core\\.buildapi)",
  "sev": "warn",
  "msg": "build-backend is not flit_core",
  "fix": "build-backend = \"flit_core.buildapi\"",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP02",
  "check": "setuptools_requires",
  "files": [
   "pyproject.toml"
  ],
  "re": "^\\s*requires\\s*=.*\\b(setuptools|wheel|hatchling|poetry-core|poetry_core|pdm-backend)\\b",
  "sev": "warn",
  "msg": "[build-system] requires pulls another backend",
  "fix": "requires = [\"flit_core \u003e=3.11,\u003c5\"]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP03",
  "check": "tool_flit_metadata",
  "files": [
   "pyproject.toml"
  ],
  "re": "^\\s*\\[tool\\.flit\\.metadata(\\.[\\w-]+)?\\]",
  "sev": "error",
  "msg": "[tool.flit.metadata] is no longer read by flit_core 4.0 (on PyPI 2026-08-04)",
  "fix": "move the fields into [project], or pin requires = [\"flit_core \u003e=2,\u003c4\"]",
  "source": "https://flit.pypa.io/en/stable/history.html"
 },
 {
  "id": "PP04",
  "check": "tool_flit_scripts",
  "files": [
   "pyproject.toml"
  ],
  "re": "^\\s*\\[tool\\.flit\\.(scripts|entrypoints[\\w.\\\"-]*)\\]",
  "sev": "error",
  "msg": "old Flit 3 scripts/entrypoints table (belongs to the [tool.flit.metadata] layout)",
  "fix": "[project.scripts] and [project.entry-points.\"group\"]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP05",
  "check": "flit_core_no_upper",
  "files": [
   "pyproject.toml"
  ],
  "re": null,
  "sev": "warn",
  "msg": "flit_core requirement has no \u003c5 upper bound",
  "fix": "requires = [\"flit_core \u003e=3.11,\u003c5\"] (\"all packages should specify \u003c5\")",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP06",
  "check": "flit_core_lt4",
  "files": [
   "pyproject.toml"
  ],
  "re": null,
  "sev": "warn",
  "msg": "flit_core \u003c4 pin on a [project]-table package",
  "fix": "change \u003c4 to \u003c5: flit_core 4.0 only drops [tool.flit.metadata]; [project] packages build unchanged",
  "source": "https://flit.pypa.io/en/stable/history.html"
 },
 {
  "id": "PP07",
  "check": "spdx_needs_311",
  "files": [
   "pyproject.toml"
  ],
  "re": null,
  "sev": "error",
  "msg": "license = \"SPDX\" string needs flit_core \u003e=3.11 but the requirement allows older",
  "fix": "requires = [\"flit_core \u003e=3.11,\u003c5\"]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP08",
  "check": "spdx_with_needs_41",
  "files": [
   "pyproject.toml"
  ],
  "re": null,
  "sev": "error",
  "msg": "license expression with WITH needs flit_core \u003e=4.1 (released 2026-09-16)",
  "fix": "requires = [\"flit_core \u003e=4.1,\u003c5\"]",
  "source": "https://flit.pypa.io/en/stable/history.html"
 },
 {
  "id": "PP09",
  "check": "import_names_needs_4",
  "files": [
   "pyproject.toml"
  ],
  "re": null,
  "sev": "error",
  "msg": "import-names / import-namespaces need flit_core \u003e=4",
  "fix": "requires = [\"flit_core \u003e=4,\u003c5\"]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP10",
  "check": "bad_dynamic",
  "files": [
   "pyproject.toml"
  ],
  "re": null,
  "sev": "error",
  "msg": "dynamic lists a field Flit cannot fill (only version, description, import-names, import-namespaces)",
  "fix": "write that field out in [project]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP11",
  "check": "console_scripts_group",
  "files": [
   "pyproject.toml"
  ],
  "re": "^\\s*\\[project\\.entry-points\\.\\\"?(console_scripts|gui_scripts)\\\"?\\]",
  "sev": "error",
  "msg": "[project.entry-points.console_scripts] is not allowed",
  "fix": "[project.scripts] (or [project.gui-scripts])",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP12",
  "check": "tool_setuptools",
  "files": [
   "pyproject.toml"
  ],
  "re": "^\\s*\\[tool\\.setuptools[\\w.\\\"-]*\\]",
  "sev": "info",
  "msg": "[tool.setuptools] table has no effect under flit_core",
  "fix": "package-data: delete (automatic); packages.find/package-dir: [tool.flit.module] name = \"importname\"; data-files: [tool.flit.external-data]",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 },
 {
  "id": "PP13",
  "check": "tool_hatch_poetry",
  "files": [
   "pyproject.toml"
  ],
  "re": "^\\s*\\[tool\\.(hatch|poetry)[\\w.\\\"-]*\\]",
  "sev": "info",
  "msg": "[tool.hatch]/[tool.poetry] table is ignored by flit_core",
  "fix": "dependencies/scripts go into [project]; build.targets.sdist include/exclude becomes [tool.flit.sdist] include/exclude",
  "source": "https://flit.pypa.io/en/stable/pyproject_toml.html"
 }
];</script>
<script>/* Flit Migration Check engine: setup.py / setup.cfg / pyproject.toml -> flit_core 4 map. Runs in Node and in the browser. */
(function () {
  var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.FLIT_RULES;
  var DV = '_' + '_version_' + '_';
  var DYN_OK = ['version', 'description', 'import-names', 'import-namespaces'];

  function detectKind(text, opts) {
    var f = (opts && opts.filename) ? String(opts.filename).toLowerCase() : '';
    if (/pyproject\.toml$/.test(f)) return 'pyproject.toml';
    if (/setup\.cfg$/.test(f)) return 'setup.cfg';
    if (/\.py$/.test(f)) return 'setup.py';
    if (/^\s*\[(build-system|project|tool\.[\w-]+)[\].]/m.test(text)) return 'pyproject.toml';
    if (/^\s*\[(metadata|options[\w.]*)\]/m.test(text)) return 'setup.cfg';
    if (/\bsetup\s*\(|\bimport\s+setuptools|\bfrom\s+(setuptools|distutils)/.test(text)) return 'setup.py';
    return '';
  }

  function ver(s) {
    var p = String(s).split('.').map(function (x) { return parseInt(x, 10) || 0; });
    return (p[0] || 0) * 10000 + (p[1] || 0) * 100 + (p[2] || 0);
  }

  function fmt(rule) {
    return (rule.msg + ' -> ' + rule.fix).split('{DV}').join(DV);
  }

  function check(text, opts) {
    opts = opts || {};
    var kind = detectKind(text, opts);
    var findings = [];
    if (!kind) return { kind: '', findings: findings, blockers: 0, ready: false };
    var lines = String(text).split(/\r?\n/);
    var byId = {};
    RULES.forEach(function (r) { byId[r.id] = r; });
    function add(rule, line) {
      findings.push({ check: rule.id + ' ' + rule.check, sev: rule.sev, msg: fmt(rule), line: line, fix: rule.fix.split('{DV}').join(DV), source: rule.source });
    }
    var section = '';
    var st = { reqLine: 0, req: '', inReq: false, licLine: 0, lic: '', impLine: 0, hasProject: false, hasFlitMeta: false };
    lines.forEach(function (raw, i) {
      var ln = i + 1;
      var t = raw.trim();
      if (!t || t.charAt(0) === '#' || (kind === 'setup.cfg' && t.charAt(0) === ';')) return;
      var h = /^\[([^\]]+)\]/.exec(t);
      if (h && kind !== 'setup.py') section = h[1].trim();
      RULES.forEach(function (r) {
        if (!r.re || r.files.indexOf(kind) < 0) return;
        if (new RegExp(r.re).test(raw)) add(r, ln);
      });
      if (kind !== 'pyproject.toml') return;
      if (section === 'project') st.hasProject = true;
      if (/^tool\.flit\.metadata/.test(section)) st.hasFlitMeta = true;
      if (section === 'build-system' && /^requires\s*=/.test(t)) { st.reqLine = ln; st.inReq = true; st.req = ''; }
      if (st.inReq) { st.req += ' ' + t; if (/\]/.test(t)) st.inReq = false; }
      if (section === 'project') {
        var lic = /^license\s*=\s*"([^"]*)"/.exec(t);
        if (lic) { st.licLine = ln; st.lic = lic[1]; }
        if (/^import-(names|namespaces)\s*=/.test(t) && !st.impLine) st.impLine = ln;
        var dyn = /^dynamic\s*=\s*\[([^\]]*)\]/.exec(t);
        if (dyn) {
          var items = (dyn[1].match(/"([^"]+)"|'([^']+)'/g) || []).map(function (s) { return s.slice(1, -1); });
          if (items.indexOf('import-names') >= 0 || items.indexOf('import-namespaces') >= 0) st.impLine = st.impLine || ln;
          var bad = items.filter(function (x) { return DYN_OK.indexOf(x) < 0; });
          if (bad.length) {
            var r10 = byId.PP10;
            findings.push({ check: r10.id + ' ' + r10.check, sev: r10.sev, msg: fmt(r10) + ' (' + bad.join(', ') + ')', line: ln, fix: r10.fix, source: r10.source });
          }
        }
      }
    });
    if (kind === 'pyproject.toml') {
      var m = /flit[_-]core\s*([^"'\]]*)/i.exec(st.req);
      if (m) {
        var spec = m[1];
        var lo = /(?:>=|~=|==)\s*([\d.]+)/.exec(spec);
        var lower = lo ? ver(lo[1]) : 0;
        if (/<\s*4\b/.test(spec)) { if (st.hasProject && !st.hasFlitMeta) add(byId.PP06, st.reqLine); }
        else if (!/<\s*5\b/.test(spec)) add(byId.PP05, st.reqLine);
        if (st.licLine && lower < ver('3.11')) add(byId.PP07, st.licLine);
        if (st.licLine && /\bWITH\b/.test(st.lic) && lower < ver('4.1')) add(byId.PP08, st.licLine);
        if (st.impLine && lower < ver('4')) add(byId.PP09, st.impLine);
      }
    }
    findings.sort(function (a, b) { return a.line - b.line; });
    var blockers = findings.filter(function (f) { return /BLOCKER/.test(f.msg); }).length;
    return { kind: kind, findings: findings, blockers: blockers, ready: findings.length === 0 };
  }

  var api = { engine: { check: check, detectKind: detectKind }, RULES: RULES, RULE_COUNT: RULES.length };
  if (typeof module !== 'undefined') module.exports = api;
  if (typeof window !== 'undefined') window.FLITENGINE = api;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
  const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
  return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
  const f = (res && res.findings) || [];
  const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
  f.forEach(function (x) { lines.push('  ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + '  ' + (x.line ? 'L' + x.line + '  ' : '') + (x.check || x.id || '') + '  ' + (x.msg || x.message || '')); });
  return lines.join('\n');
}
function toHtml(res) {
  const f = (res && res.findings) || [];
  if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
  const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
  return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var FLITREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = FLITREPORT;
else window.FLITREPORT = FLITREPORT;
</script>
<script>
(function(){
  var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
  today.value=new Date().toISOString().slice(0,10);
  function run(){ try { var res=window.FLITENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.FLITREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
  go.onclick=run; if (src.value.trim()) run();
})();
</script>


<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/flit-migration-check" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'flit-migration-check',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Flit Migration Check: setup.py to flit_core 4 every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"flit-migration-check",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);document.addEventListener("click",function(ev){var b=ev.target&&ev.target.closest?ev.target.closest("button,input[type=submit]"):null;if(b&&!b.closest("[data-yj],[data-yj-cta],[data-yj-install],[data-yj-alert],.yj-find,.yj-related,#yj-ask,.yjp"))mv(ev);},true);})();</script></section>

<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the full version <span class="yj-price">$29</span></h2><p class="yj-cta-paid" style="margin:.25rem 0 .6rem;color:#1f2a37">Workspace key ($29 once): every setup.py, setup.cfg and pyproject.toml in the workspace as one dated migration plan, each item mapped to its flit_core 4 replacement. Team key ($149): licence and end-of-life exposure report across projects plus a CI gate that fails a build adding new setuptools-only </p><p class="yj-cta-pay" style="margin:.25rem 0 1rem;color:#5b6b7c;font-size:.94rem">One payment, one licence key for this tool. The key is shown right after payment.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_VI4ER6LWKbRiLZHzzqUpAK77hPReaZmifZSkj19xgaN" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>

<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-30">Updated 2026-09-30</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-30","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku428290_result_card.jpg" alt="Flit Migration Check: setup.py to flit_core 4" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Six setup.py lines from a sample Python package show what a Flit migration really is: install_requires becomes [project] dependencies, extras_require becomes [project.optional-dependencies], a console_scripts entry becomes [project.scripts], data_files becomes [tool.flit.external-data], python_requires becomes requires-python, and license=&quot;MIT&quot; becomes an SPDX license string that needs flit_core &gt;=3.11. If you maintain a pure-Python library on PyPI and you are moving from setuptools, Hatch or the old Flit 3 layout to flit_core 4, this is the list you would otherwise write by hand.</p>
<h3>Migrate from setuptools to Flit: what changed in August and September 2026</h3>
<p>flit_core 4.0 (on PyPI 2026-08-04) no longer reads [tool.flit.metadata]. The Flit release history says it plainly: the old table &quot;is no longer used for project metadata&quot;; use the [project] table, or build with flit_core &lt;4. A package whose [build-system] asks for &quot;flit_core &gt;=3.2&quot; with no upper bound now receives flit_core 4 when pip builds it from the sdist, and the build stops.</p>
<p>flit_core 4.1 (on PyPI 2026-09-16) accepts license expressions with WITH, such as &quot;Apache-2.0 WITH LLVM-exception&quot;. That line needs &quot;flit_core &gt;=4.1,&lt;5&quot; in requires. A plain SPDX expression like &quot;MIT&quot; needs flit_core &gt;=3.11, and import-names needs flit_core &gt;=4. The Flit docs also ask every package to pin &quot;&lt;5&quot;.</p>
<h3>What the checker does</h3>
<p>Flit Migration Check is a VS Code extension with a free web version that runs the same engine. Open setup.py, setup.cfg or pyproject.toml and it prints one finding per line: the line number, what the line is, and the flit_core 4 line that replaces it. It has 38 rules, and each one names its source page on flit.pypa.io.</p>
<p>The bundled sample setup.py gives 22 findings on 22 lines (10 warnings, 12 notes, 0 blockers). A finished flit_core 4 pyproject.toml gives 0 findings, which is the point where you delete setup.py.</p>
<p>Three pyproject.toml cases show how the answer changes with the input:</p>
<ul><li>An old Flit 3 file with [tool.flit.metadata], [tool.flit.scripts] and &quot;flit_core &gt;=3.2&quot;: 3 findings. The metadata table, the scripts table, and the missing &lt;5 bound.</li><li>A Hatch file with hatchling, dynamic = [&quot;version&quot;, &quot;dependencies&quot;] and [project.entry-points.console_scripts]: 5 findings. Flit fills only version, description, import-names and import-namespaces from dynamic, and console_scripts belongs in [project.scripts].</li><li>A Flit 3.x file pinned &quot;flit_core &gt;=3.2,&lt;4&quot; with license = &quot;Apache-2.0 WITH LLVM-exception&quot;: 3 findings. The &lt;4 pin can become &lt;5, and the WITH license needs flit_core &gt;=4.1.</li></ul>
<h3>Blockers: when not to use Flit</h3>
<p>Flit builds pure Python only: &quot;If your package needs a build step, you won&#x27;t be able to use Flit&quot;. The checker marks ext_modules, Extension(, cythonize( and custom cmdclass as BLOCKER lines. Version from git tags (setuptools_scm, versioneer) is also a blocker: write the version into the package and add dynamic = [&quot;version&quot;].</p>
<h3>Before twine upload</h3>
<p>PyPI rejects an upload file over 100.0 MiB by default, and it refuses invalid Trove classifiers and a name too close to an existing project. The classifiers rule reminds you that &quot;Private :: Do Not Upload&quot; blocks an accidental upload.</p>
<h3>Free and paid</h3>
<p>The single-file map is free with no limit. A workspace key turns every setup.py, setup.cfg and pyproject.toml in the workspace into one dated migration plan. A team key adds an exposure report across projects and a CI gate that fails a build adding new setuptools-only use.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku428290_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku428290_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Flit Migration Check do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the open setup.py, setup.cfg or pyproject.toml in VS Code and lists every line that still belongs to setuptools, Hatch, Poetry or the old Flit 3 layout, with its line number and the exact flit_core 4 line that replaces it. Compiled extensions and custom cmdclass are marked BLOCKER because Flit builds pure Python only. It has 38 rules.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is Flit Migration Check for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Python library maintainers moving a pure-Python package from setup.py or setuptools to Flit, teams whose Flit 3 packages still use [tool.flit.metadata], and anyone switching from Hatch or Poetry to flit_core. flit_core 4.0 (on PyPI 2026-08-04) no longer reads [tool.flit.metadata], so an unpinned old-layout package no longer builds from its sdist.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask a chatbot or read the Flit docs?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The docs are free and correct, but you must map each setup() argument yourself. A chatbot&#x27;s packaging advice from before August 2026 cannot know that flit_core 4.0 (on PyPI 2026-08-04) no longer reads [tool.flit.metadata], or that flit_core 4.1 (on PyPI 2026-09-16) accepts license expressions with WITH. The checker reads your actual file line by line, offline, and quotes the flit docs for each replacement.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The single-file map is free with no limit: every finding, line number and replacement line; the bundled sample setup.py gives 22 findings on 22 lines (10 warnings, 12 notes, 0 blockers). A $29 workspace key adds one dated migration plan across every setup.py, setup.cfg and pyproject.toml. A $149 team key adds the cross-project exposure report and a CI gate that fails a build adding new setuptools-only use.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does it cost compared with doing it by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">By hand you read three flit.pypa.io pages and translate each setup() argument, then learn about a wrong flit_core bound when the sdist build fails. The free map does the translation per file. The workspace key is $29 once, one licence key per person or team seat, and it is not a subscription.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Flit Migration Check do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the open setup.py, setup.cfg or pyproject.toml in VS Code and lists every line that still belongs to setuptools, Hatch, Poetry or the old Flit 3 layout, with its line number and the exact flit_core 4 line that replaces it. Compiled extensions and custom cmdclass are marked BLOCKER because Flit builds pure Python only. It has 38 rules."}}, {"@type": "Question", "name": "Who is Flit Migration Check for?", "acceptedAnswer": {"@type": "Answer", "text": "Python library maintainers moving a pure-Python package from setup.py or setuptools to Flit, teams whose Flit 3 packages still use [tool.flit.metadata], and anyone switching from Hatch or Poetry to flit_core. flit_core 4.0 (on PyPI 2026-08-04) no longer reads [tool.flit.metadata], so an unpinned old-layout package no longer builds from its sdist."}}, {"@type": "Question", "name": "Why not just ask a chatbot or read the Flit docs?", "acceptedAnswer": {"@type": "Answer", "text": "The docs are free and correct, but you must map each setup() argument yourself. A chatbot's packaging advice from before August 2026 cannot know that flit_core 4.0 (on PyPI 2026-08-04) no longer reads [tool.flit.metadata], or that flit_core 4.1 (on PyPI 2026-09-16) accepts license expressions with WITH. The checker reads your actual file line by line, offline, and quotes the flit docs for each replacement."}}, {"@type": "Question", "name": "What is free and what needs a key?", "acceptedAnswer": {"@type": "Answer", "text": "The single-file map is free with no limit: every finding, line number and replacement line; the bundled sample setup.py gives 22 findings on 22 lines (10 warnings, 12 notes, 0 blockers). A $29 workspace key adds one dated migration plan across every setup.py, setup.cfg and pyproject.toml. A $149 team key adds the cross-project exposure report and a CI gate that fails a build adding new setuptools-only use."}}, {"@type": "Question", "name": "What does it cost compared with doing it by hand?", "acceptedAnswer": {"@type": "Answer", "text": "By hand you read three flit.pypa.io pages and translate each setup() argument, then learn about a wrong flit_core bound when the sdist build fails. The free map does the translation per file. The workspace key is $29 once, one licence key per person or team seat, and it is not a subscription."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:428290,slug:"flit-migration-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/spdx-license-field-lint"><span class="yj-rel-t">SPDX License Field Lint for package.json, pyproject.toml and Cargo.toml</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/personal-data-map-dsar-audit"><span class="yj-rel-t">Personal Data Map: find every column a GDPR subject access request has to reac</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pyproject-release-gate"><span class="yj-rel-t">pyproject.toml Release Gate (PEP 639)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/revit-door-families"><span class="yj-rel-t">Revit Door Families - UK Door Block Library in mm: 14 doors on standard 838 x </span><span class="yj-rel-p">$100</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/almalinux-migration-lint"><span class="yj-rel-t">AlmaLinux Migration Lint — CentOS 7/8</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/autohotkey-toml-devicetree-snippets"><span class="yj-rel-t">AutoHotkey v2, TOML and DeviceTree checks for VS Code</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>