Skip to content

feat: populate component.authors from pyproject.toml, Poetry manifests and packaging metadata - #1096

Open
SemyonAndreyev wants to merge 2 commits into
CycloneDX:mainfrom
q2quantum:feat/component-authors
Open

feat: populate component.authors from pyproject.toml, Poetry manifests and packaging metadata#1096
SemyonAndreyev wants to merge 2 commits into
CycloneDX:mainfrom
q2quantum:feat/component-authors

Conversation

@SemyonAndreyev

Copy link
Copy Markdown

Description

Populates component.authors (CycloneDX 1.6+, structured, repeatable) and derives the legacy singular component.author for the single-author case, from three independent sources: PEP 621 project.authors, Poetry's tool.poetry.authors, and packaging core-metadata Author/Author-email (wired into the environment scan).

New shared helper module cyclonedx_py/_internal/utils/contact.py:

  • person_string2contact() parses the "Name <email>" convention used by both Poetry and packaging metadata.
  • contacts2author() derives the legacy author string only when there is exactly one author — CycloneDX has no agreed-upon way to fold multiple authors into one string (see CycloneDX/specification#335), so this deliberately does not guess a join convention and leaves it unset otherwise.

Along the way, found and handled two real edge cases:

  • email.utils.getaddresses() silently mis-splits a bare name with no @ on whitespace, keeping only the last word (getaddresses(['Jane Doe']) == [('', 'Jane')]). metadata2authors() filters out any parsed address that doesn't actually contain @ before trusting it.
  • PEP 621 requires authors to be tables, but tests/_data/infiles/pipenv/no-deps/pyproject.toml already exercises the non-compliant authors = ["Name <email>", ...] string form some real-world files use anyway — project2authors() now falls back to the same string parser instead of raising.

56 new test cases across a new test_utils_contact.py, new test_utils_poetry.py, new test_utils_packaging.py, and an extended test_utils_pep621.py. Full existing suite passes with only the expected snapshot updates (root components in fixtures that declare authors now show them). flake8/isort/mypy clean.

Resolves or fixes issue: #648

AI Tool Disclosure

  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: Claude Code
    • LLMs and versions: Claude Sonnet 5
    • Prompts: Implement the three data sources for issue feat: populate component.authors #648 as specified by the maintainer's own comments on the issue; add thorough test coverage; verify against the full existing test suite with no regressions.

Affirmation

🤖 Generated with Claude Code

…s and packaging metadata

Fixes CycloneDX#648.

Adds the structured, repeatable `components[].authors` (CycloneDX 1.6+)
and derives the legacy singular `components[].author` string for the
single-author case, from three independent sources:

- PEP 621 `project.authors` (pep621.py) -- tables of name/email, with
  lenient handling of the non-compliant-but-real-world "Name <email>"
  string form some pyproject.toml files use here instead.
- Poetry's `tool.poetry.authors` (utils/poetry.py) -- "Name <email>"
  strings.
- Packaging core-metadata's `Author`/`Author-email` (utils/packaging.py,
  wired into the environment scan) -- the two are independent free-text
  fields with no guaranteed correlation when either holds more than one
  person; see the docstring of `metadata2authors()` for the rules used
  to combine or keep them separate.

New shared helper module `utils/contact.py`:
- `person_string2contact()` parses the "Name <email>" convention used
  by both Poetry and packaging metadata.
- `contacts2author()` derives the legacy `author` string only when
  there is exactly one author -- CycloneDX has no agreed-upon way to
  fold multiple authors into one string (see
  CycloneDX/specification#335), so this deliberately does not guess a
  join convention.

Found and worked around a real footgun in `email.utils.getaddresses()`:
fed a bare name with no `@`, it silently mis-splits on whitespace and
keeps only the last word (`getaddresses(['Jane Doe']) == [('', 'Jane')]`).
`metadata2authors()` filters out any parsed address that doesn't
actually contain `@` before trusting it.

Tests: 56 new cases across a new `test_utils_contact.py`, new
`test_utils_poetry.py`, new `test_utils_packaging.py`, and an extended
`test_utils_pep621.py` -- covering both happy paths and the edge cases
above. Full existing suite passes unchanged aside from the expected
snapshot updates (root components in fixtures that declare authors now
show them).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xJ8pA9rPpAjVATkZCzCS2
Signed-off-by: Semyon Andreev <q2quantum.app@gmail.com>
@SemyonAndreyev
SemyonAndreyev requested a review from a team as a code owner September 3, 2026 19:03
@codacy-production

codacy-production Bot commented Sep 3, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🔴 Metrics 38 complexity · 0 duplication

Metric Results
Complexity ⚠️ 38 (≤ 20 complexity)
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

…ting

Additional test coverage for the component.authors feature (CycloneDX#1096/CycloneDX#648),
found by systematically probing edge cases of the three author sources:

- contact.py: `_PERSON_STRING_MATCHER.match()` returning None was marked
  `# pragma: nocover -- the pattern matches any string, including the
  empty one`, but that comment is incorrect -- multiple `<...>` fragments,
  an unbalanced bracket, or trailing text after a closing `>` all make the
  whole match fail (verified live). Removed the stale pragma, documented
  why the branch is reachable, and added 5 new test cases exercising it
  (test_none_for_unparseable_shape).

- pep621.py: `project2authors()` already tolerates a not-per-spec *string*
  entry in `authors` (see test_project2authors_string_entry_not_per_spec_
  but_tolerated), but a non-dict, non-string entry -- e.g. `authors = [123]`,
  valid TOML, invalid PEP 621 -- crashed with AttributeError on
  `author.get(...)`. Verified live before fixing. Now skipped, consistent
  with the function's own existing "be lenient, don't crash" intent for the
  string case; 3 new test cases.

- poetry.py: same class of bug in `poetry2authors()` -- a non-string entry
  crashed with TypeError from `re.match()` inside `person_string2contact()`.
  Same fix, same reasoning; 3 new test cases.

metadata2authors() in packaging.py was also checked for a similar gap but
already has thorough dedicated coverage matching its own documented rules
(9 existing tests); no gap found there.

68/68 tests pass across the 4 affected test modules (11 new cases added).
flake8 and mypy clean on all changed files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xJ8pA9rPpAjVATkZCzCS2
Signed-off-by: Semyon Andreev <q2quantum.app@gmail.com>
@SemyonAndreyev

Copy link
Copy Markdown
Author

Pushed a follow-up commit while stress-testing this feature's edge cases further: found and fixed two real crashes on not-per-spec (but valid TOML) authors entries —

  • project2authors() (PEP 621): a non-dict, non-string entry (e.g. authors = [123]) crashed with AttributeError on author.get(...).
  • poetry2authors() (Poetry): a non-string entry crashed with TypeError from re.match().

Both are now skipped, consistent with this same code's existing "be lenient, don't crash" handling of a not-per-spec string entry in the PEP 621 case. Also removed a stale # pragma: nocover comment on person_string2contact()'s None-return branch — it claimed the regex "matches any string," which isn't actually true for several realistic messy shapes (multiple <...> fragments, unbalanced brackets, trailing text after a closing >); added 5 test cases exercising it.

11 new test cases, 68/68 passing across the 4 affected test modules. flake8/mypy clean.

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.

1 participant