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=">=3.8",
setup_requires=["wheel"],
install_requires=["pandas>=2.0", "click>=8"],
extras_require={"test": ["pytest>=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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
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 {"&":"&","<":"<",">":">",'"':"""}[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="MIT" becomes an SPDX license string that needs flit_core >=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 "is no longer used for project metadata"; use the [project] table, or build with flit_core <4. A package whose [build-system] asks for "flit_core >=3.2" 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 "Apache-2.0 WITH LLVM-exception". That line needs "flit_core >=4.1,<5" in requires. A plain SPDX expression like "MIT" needs flit_core >=3.11, and import-names needs flit_core >=4. The Flit docs also ask every package to pin "<5".</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 "flit_core >=3.2": 3 findings. The metadata table, the scripts table, and the missing <5 bound.</li><li>A Hatch file with hatchling, dynamic = ["version", "dependencies"] 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 "flit_core >=3.2,<4" with license = "Apache-2.0 WITH LLVM-exception": 3 findings. The <4 pin can become <5, and the WITH license needs flit_core >=4.1.</li></ul>
<h3>Blockers: when not to use Flit</h3>
<p>Flit builds pure Python only: "If your package needs a build step, you won't be able to use Flit". 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 = ["version"].</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 "Private :: Do Not Upload" 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'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>
|