Extension points#

A new metadata format is an Adapter plus a crosswalk mapping. Recipes has the order to do it in.

The adapter contract.

An adapter is one supported metadata file or ecosystem: the logic needed to find it, read it, check it against its own specification, and express what it says in CodeMeta concepts. Adapters know nothing about each other and nothing about comparison; they only ever translate into the common representation.

Adding support for a new ecosystem means writing one adapter and one mapping file. Nothing in the comparison engine changes, and no existing adapter is touched. See docs/developing/adapters.md.

exception rs_metadata.adapters.base.SourceError(message, line=None)[source]#

Bases: Exception

A metadata file exists but cannot be read.

Carries a line number where the underlying parser reports one, so even a syntax error gets an inline CI annotation.

Parameters:
  • message (str)

  • line (int | None)

Return type:

None

class rs_metadata.adapters.base.Adapter[source]#

Bases: ABC

One supported metadata format.

id: str#

Stable identifier, used as a filter key in the report.

format: str#

Human-readable format name.

role: str = 'optional'#

anchor for codemeta.json, required for a file whose absence is itself a finding, optional for formats that are auto-detected when a repository happens to contain them.

filenames: tuple[str, ...] = ()#

Canonical filenames, in preference order.

mapping_name: str | None = None#

Mapping file stem, or None for the anchor, which needs no crosswalk.

detect(root)[source]#

Find this format’s file in a repository, or return None.

Matching is case-insensitive because citation.cff and CITATION.cff are the same file on macOS and Windows. Whether the casing is the one GitHub actually requires is a separate question, answered by validate_source().

Parameters:

root (Path)

Return type:

Path | None

abstractmethod parse(path, relative)[source]#

Read the file. Raises SourceError if it is malformed.

Parameters:
Return type:

ParsedSource

validate_source(parsed)[source]#

Check the file against its own format’s specification.

Parameters:

parsed (ParsedSource)

Return type:

list[Diagnostic]

abstractmethod map_to_codemeta(parsed)[source]#

Express what the file says as CodeMeta concepts.

Parameters:

parsed (ParsedSource)

Return type:

dict[str, list[ConceptValue]]

property mapping: Mapping | None#

Which adapters exist, and in what order they are reported.

Registration order is reporting order. The anchor comes first, then the file required alongside it, then the optional formats that are auto-detected when a repository happens to contain them.

To add a format, write an adapter and a mapping file and add it to ADAPTERS. Nothing else in the codebase needs to change.

rs_metadata.adapters.registry.ANCHOR = <CodeMetaAdapter id='codemeta'>#

The canonical record every other source is compared against.

rs_metadata.adapters.registry.companion_adapters()[source]#

Every adapter except the anchor, in reporting order.

Returns:

tuple of Adapter – The formats compared against codemeta.json.

Return type:

tuple[Adapter, …]

Examples

>>> for adapter in companion_adapters():
...     print(adapter.id)
cff
github
zenodo
biotools
pyproject
package-json
r-description
cargo
julia-project
dockerfile

The anchor is excluded because the companions are compared to it:

>>> ANCHOR.id in {adapter.id for adapter in companion_adapters()}
False
rs_metadata.adapters.registry.get_adapter(adapter_id)[source]#

Look up a registered adapter by its identifier.

Parameters:

adapter_id (str) – The adapter’s id, as it appears in a report’s sources.

Returns:

Adapter – The registered adapter.

Raises:

KeyError – If nothing is registered under that id.

Return type:

Adapter

Examples

>>> get_adapter("cff").format
'Citation File Format'
>>> get_adapter("nonexistent")
Traceback (most recent call last):
    ...
KeyError: "No adapter registered with id 'nonexistent'."

Loading of the declarative crosswalk mappings.

A mapping file answers two questions per CodeMeta property: which part of the source format corresponds to it, and how strictly the two should be expected to agree. Keeping that as data rather than as code means a new rule is a reviewable diff in a YAML file, and means the report can state exactly which crosswalk produced a finding and whether that crosswalk came from CodeMeta upstream or from rs-metadata.

The mapping never decides whether two values are equivalent. That is the normalization layer’s job, and it is shared across all formats.

class rs_metadata.mapping.Rule(*values)[source]#

Bases: StrEnum

How strictly a concept is expected to agree across two files.

A StrEnum rather than a validated string, matching Severity: Rule(value) rejects an unknown rule on its own, and a typo in a mapping file fails at load time rather than silently comparing as something else.

Examples

>>> Rule("subset")
<Rule.SUBSET: 'subset'>
>>> Rule.EQUAL == "equal"
True

An unknown rule is rejected without any extra validation code:

>>> Rule("nearly")
Traceback (most recent call last):
    ...
ValueError: 'nearly' is not a valid Rule
EQUAL = 'equal'#
SUBSET = 'subset'#
OVERLAP = 'overlap'#
INFORMATIONAL = 'informational'#
property label: str#

How this rule reads in a report or a crosswalk table.

Examples

>>> Rule.SUBSET.label
'source ⊆ anchor'
class rs_metadata.mapping.Origin(*values)[source]#

Bases: StrEnum

Where a crosswalk came from.

A finding carries this so a reader can tell a mapping CodeMeta publishes from one this project decided on.

UPSTREAM = 'codemeta-upstream'#
RS_METADATA = 'rs-metadata'#
MIXED = 'mixed'#
property label: str#

How this origin reads in a crosswalk table.

Examples

>>> Origin.UPSTREAM.label
'CodeMeta upstream crosswalk'
class rs_metadata.mapping.FieldRule(property, source, strategy='text', rule=Rule.EQUAL, severity=Severity.ERROR, comparable=True, reason=None, note=None, anchor_properties=())[source]#

Bases: object

The comparison policy for one CodeMeta concept in one source format.

Parameters:
property: str#
source: str#
strategy: str = 'text'#
rule: Rule = 'equal'#
severity: Severity = 'error'#
comparable: bool = True#
reason: str | None = None#
note: str | None = None#
anchor_properties: tuple[str, ...] = ()#

Anchor concepts this source property is compared against. Usually just property, but ecosystems draw role boundaries differently: npm’s author may legitimately appear as a CodeMeta maintainer.

anchors()[source]#

Which anchor concepts this source property is compared against.

Returns:

tuple of str – The declared anchor properties, or just property.

Return type:

tuple[str, …]

Examples

By default a concept is compared against itself:

>>> FieldRule(property="version", source="version").anchors()
('version',)

Ecosystems that draw a role boundary differently declare both sides, so an npm author can legitimately be a CodeMeta maintainer:

>>> FieldRule(property="author", source="author",
...           anchor_properties=("author", "maintainer")).anchors()
('author', 'maintainer')
class rs_metadata.mapping.Mapping(id, format, format_version, reference, fields, notes=None)[source]#

Bases: object

One crosswalk between a source format and CodeMeta.

Parameters:
id: str#
format: str#
format_version: str | None#
reference: MappingRef#
fields: dict[str, FieldRule]#
notes: str | None = None#
rule_for(concept)[source]#
Parameters:

concept (str)

Return type:

FieldRule | None

rs_metadata.mapping.load(name)[source]#

Load a crosswalk mapping by file stem.

Parameters:

name (str) – Mapping file stem, for example cff-1.2.0.

Returns:

Mapping – The parsed crosswalk. Cached, since mappings are immutable.

Raises:

ValueError – If the mapping declares a property twice, or uses an unknown rule.

Return type:

Mapping

Examples

>>> mapping = load("cff-1.2.0")
>>> mapping.format, mapping.format_version
('Citation File Format', '1.2.0')

Provenance travels with the mapping onto every finding it produces:

>>> mapping.reference.origin
<Origin.UPSTREAM: 'codemeta-upstream'>

Rules are addressed by CodeMeta concept:

>>> rule = mapping.rule_for("version")
>>> rule.source, rule.strategy, rule.rule.value, rule.severity.value
('version', 'version', 'equal', 'error')

A concept the format has no field for simply has no rule:

>>> mapping.rule_for("programmingLanguage") is None
True
rs_metadata.mapping.available()[source]#

Every mapping shipped with the package, for tests and documentation.

Return type:

list[str]