Skip to content

fix(flit): don't emit null values when importing a flit project - #3832

Merged
frostming merged 1 commit into
pdm-project:mainfrom
shuvamk:fix/flit-import-null-values
Aug 11, 2026
Merged

fix(flit): don't emit null values when importing a flit project#3832
frostming merged 1 commit into
pdm-project:mainfrom
shuvamk:fix/flit-import-null-values

Conversation

@shuvamk

@shuvamk shuvamk commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Pull Request Checklist

  • A news fragment is added in news/ describing what is new.
  • Test cases added for changed code.

Describe what you have changed in this PR.

The problem

Two optional keys in [tool.flit.*] make pdm import abort. In both cases the importer
builds a value of None, which tomlkit cannot represent, so the import fails and nothing
is written to pyproject.toml.

1. A modern PEP 621 flit project that sets only exclude in [tool.flit.sdist]

The project being imported — no [tool.flit.metadata] at all, this is the layout
flit_core ≥ 3.2 recommends:

[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"

[project]
name = "mymod"
version = "0.1.0"
description = "A modern flit project"
authors = [{name = "Test Author", email = "t@example.com"}]
requires-python = ">=3.9"
dependencies = ["requests>=2.6"]

[tool.flit.sdist]
exclude = ["doc/*.html"]

check_fingerprint only looks for a tool.flit table, so a bare pdm import auto-detects
this as flit — no -f needed:

=== pdm import (bare, auto-detect) with flit.py from: main ===
[ConvertError]: Unable to convert an object of <class 'NoneType'> to a TOML item
WARNING: Add '-v' to see the detailed traceback
--- resulting [tool.pdm.build] ---
(absent)

=== pdm import (bare, auto-detect) with flit.py from: thispr ===
Changes are written to pyproject.toml.
--- resulting [tool.pdm.build] ---
[tool.pdm.build]
excludes = ["doc/*.html"]

Setting only one of the two keys is valid: flit_core reads them independently, each
defaulting to []flit_core/config.py:153-161, flit_core 3.12.0:

loaded_cfg.sdist_include_patterns = _check_glob_patterns(
    dtool['sdist'].get('include', []), 'include'
)
exclude = [
    "**/__pycache__",
    "**.pyc",
] + dtool['sdist'].get('exclude', [])
loaded_cfg.sdist_exclude_patterns = _check_glob_patterns(
    exclude, 'exclude'
)

2. A legacy-layout project with an author name and no email

[tool.flit.metadata]
module = "mymod"
author = "Test Author"
$ pdm import -f flit ../pyproject.toml
[MetaConvertError]:
name: Unable to convert an object of <class 'NoneType'> to a TOML item

Also valid: author-email is in flit_core's metadata_allowed_fields but
metadata_required_fields is only {module, author}flit_core/config.py:51-54,
flit_core 3.12.0:

metadata_required_fields = {
    'module',
    'author',
}

Same for maintainer without maintainer-email.

input expected main @ b3cb02ed
PEP 621 project, [tool.flit.sdist] with only exclude [tool.pdm.build] excludes = [...] ConvertError, import aborts, nothing written
PEP 621 project, [tool.flit.sdist] with only include [tool.pdm.build] includes = [...] ConvertError, import aborts, nothing written
author without author-email authors = [{name = "Test Author"}] MetaConvertError, import aborts
maintainer without maintainer-email maintainers = [{name = "..."}] MetaConvertError, import aborts

The cause

Both sites write the optional key unconditionally.

src/pdm/formats/flit.py:38-40:

name = metadata.pop(type_)
email = metadata.pop(f"{type_}-email", None)
return cast(list[str], array_of_inline_tables([{"name": name, "email": email}]))

src/pdm/formats/flit.py:140-142:

self.settings.setdefault("build", {}).update(
    {"excludes": value.get("exclude"), "includes": value.get("include")}
)

When the source omits the key, email — respectively the missing get() — is None.
tomlkit.inline_table().update(...) raises immediately for the author case; the sdist
case survives conversion and blows up when [tool.pdm.build] is written in
import_cmd.do_import.

The fix

Only set the key when the source provides it. This is what the Poetry importer already
does for a missing email: parse_name_email filters out the None groups of the
name/email regex match (src/pdm/formats/poetry.py:116-126).

The excludes-before-includes insertion order is deliberately preserved, so a project
that sets both keys — the common case, and what the flit-demo fixture uses —
generates a byte-identical file. Running pdm import -f flit on
tests/fixtures/projects/flit-demo/pyproject.toml under both source versions:

### main vs this PR ###
no differences
5edda2664b636d03c3f7488ab55939fedf3d0daf37769ae83c40a05114a29ae7  out_main/pyproject.toml
5edda2664b636d03c3f7488ab55939fedf3d0daf37769ae83c40a05114a29ae7  out_thispr/pyproject.toml

Tests

Two new tests in tests/test_formats.py, next to test_convert_flit:

  • test_convert_flit_author_and_maintainer_without_email
  • test_convert_flit_sdist_with_one_of_include_and_exclude, parametrised over
    include-only and exclude-only

The sdist fixture carries an author-email on purpose, so it fails for the sdist reason
and not the author one — the two halves are proven independently.

I verified all three fail without the fix. Reverting only src/pdm/formats/flit.py to
origin/main and keeping the tests:

    def test_convert_flit_author_and_maintainer_without_email(project, tmp_path):
>       result, _ = flit.convert(project, pyproject_file, None)
>           raise MetaConvertError(errors, data=self._data, settings=self.settings)
E           pdm.formats.base.MetaConvertError:
E           name: Unable to convert an object of <class 'NoneType'> to a TOML item

    def test_convert_flit_sdist_with_one_of_include_and_exclude(project, tmp_path, sdist_table, expected_build):
>       assert settings["build"] == expected_build
E       AssertionError: assert {'excludes': ...es': ['doc/']} == {'includes': ['doc/']}
E         Left contains 1 more item:
E         {'excludes': None}
E         Full diff:
E           {
E         +     'excludes': None,
E               'includes': [
E                   'doc/',
E               ],
E           }

    def test_convert_flit_sdist_with_one_of_include_and_exclude(project, tmp_path, sdist_table, expected_build):
>       assert settings["build"] == expected_build
E       AssertionError: assert {'excludes': ...cludes': None} == {'excludes': ['doc/*.html']}
E         Left contains 1 more item:
E         {'includes': None}
E         Full diff:
E           {
E               'excludes': [
E                   'doc/*.html',
E               ],
E         +     'includes': None,
E           }

3 failed, 21 deselected in 0.93s

Restoring the file: 4 passed, 20 deselected in 0.74s (the three new ones plus the
pre-existing test_convert_flit).

Beyond the unit tests I ran the real CLI against both inputs in a scratch project; the
transcripts above are that run.

What I ran

command main @ b3cb02ed this PR
pytest -n auto -m "not integration and not network" -q 1292 passed, 1 skipped 1295 passed, 1 skipped
prek run --all-files ruff / ruff format / codespell / mypy all Passed all Passed

The one skip is tests/test_utils.py:171 (Windows-only) on both. I did not run the
integration/network marked tests or the tox matrix.


Disclosure: I used an AI coding assistant (Claude Code) to find this and to help write
the fix.

Two optional keys in `[tool.flit.*]` leaked `None` into the metadata handed
to tomlkit, which cannot represent it. In both cases the import aborts and
nothing is written.

flit_core reads `include` and `exclude` from `[tool.flit.sdist]`
independently, each defaulting to `[]` (config.py:153-161, flit_core
3.12.0), so setting only one is valid. The importer wrote both through
`dict.get`, so the missing one became `None` and the write of
`[tool.pdm.build]` failed. This hits modern PEP 621 flit projects, which
carry no `[tool.flit.metadata]` at all:

    [build-system]
    requires = ["flit_core >=3.2,<4"]
    build-backend = "flit_core.buildapi"

    [project]
    name = "mymod"
    ...

    [tool.flit.sdist]
    exclude = ["doc/*.html"]

    $ pdm import ../pyproject.toml
    [ConvertError]: Unable to convert an object of <class 'NoneType'> to
    a TOML item

`check_fingerprint` only looks for a `tool.flit` table, so this is what a
bare auto-detecting `pdm import` does with such a project.

Similarly, `author-email` and `maintainer-email` are in flit_core's
`metadata_allowed_fields` but not in `metadata_required_fields`, which is
only `{module, author}` (config.py:51-54), so a legacy-layout project with
an author name and no email is valid. `_get_author` built the inline table
unconditionally:

    [tool.flit.metadata]
    module = "mymod"
    author = "Test Author"

    $ pdm import -f flit ../pyproject.toml
    [MetaConvertError]: name: Unable to convert an object of
    <class 'NoneType'> to a TOML item

Only set the key when the source provides it, as the Poetry importer's
`parse_name_email` already does when dropping an absent email. The
`excludes`/`includes` insertion order is kept as it was so that a project
setting both keys generates a byte-identical `[tool.pdm.build]`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shuvamk
shuvamk force-pushed the fix/flit-import-null-values branch from de73ffa to 717856f Compare August 2, 2026 05:49
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.28%. Comparing base (b3cb02e) to head (717856f).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3832      +/-   ##
==========================================
+ Coverage   88.16%   88.28%   +0.12%     
==========================================
  Files         121      121              
  Lines       13221    13227       +6     
  Branches     2246     2249       +3     
==========================================
+ Hits        11656    11678      +22     
+ Misses        982      975       -7     
+ Partials      583      574       -9     
Flag Coverage Δ
unittests 88.17% <100.00%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@frostming
frostming merged commit 410a651 into pdm-project:main Aug 11, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants