All notable changes to odmlib will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.2.1 - 2026-08-27
-
A Claude Code skill for odmlib ships in the repository at
.claude/skills/odmlib/. It teaches Claude the loader-per-standard mapping, namespace registration, element ordering, the three validation layers, and the serialization pitfalls that are easy to get wrong by hand. Contents:SKILL.md, fourreferences/*.md(API reference, models, validation, Dataset-JSON), and eight runnableexamples/*.py..claude/skills/odmlib.skillis the same tree packed as a zip for distribution. -
Repo-only — it is not part of the PyPI package.
pip install odmlibdoes not install the skill;[tool.setuptools.packages.find]includesodmlib*only. Install it by copying.claude/skills/odmlib/into a project's.claude/skills/, or into~/.claude/skills/to make it available everywhere. See the Claude Code Skill sections ofREADME.mdandCLAUDE.md. -
It describes odmlib 0.2.1 and later. Several behaviors it documents do not hold on 0.2.0 — namespace-aware
to_xml_string(), opt-in context-manager writing, and full error enumeration undercollect_errors=True. The skill advises detecting capabilities rather than comparing version strings, since a pre-release sorts below its release under PEP 440. -
Guarded by tests, not just prose.
tests/test_skill_contract.pypins the skill's documented signatures, front-matter limits, import surface, schema pairs, and the list-vs-object shape rule against live introspection;tests/test_skill_examples.pyruns all eight examples;tests/test_skill_bundle.pychecks the packed.skillbundle matches the source tree by SHA-256. Repack withpython scripts/build_skill_bundle.pyafter editing any skill file. These run in a single CI cell. -
Feedback welcome via the
skill-feedback.ymlissue template ("Claude generated incorrect odmlib code"), which captures the prompt, the generated code, and the versions involved.
-
ODMElement.to_element()returns a standard, namespace-resolved ElementTree Element. Getting a tree out of odmlib previously meantto_xml(), which builds the library's internal serialization buffer: prefix-literal tags (def:leaf, not Clark notation) and noxmlnsdeclarations. That tree cannot be re-parsed on its own for Define-XML (ParseError: unbound prefix), lands in no namespace for ODM, failsET.canonicalize(), and does not support namespace-awarefind().to_element()returns a tree parsed fromto_xml_string(), so all of those work:elem = define.Study.MetaDataVersion.ItemGroupDef[0].to_element() elem.find("{http://www.cdisc.org/ns/def/v2.1}leaf") # namespace-aware find host = ET.Element("SubmissionPackage"); host.append(elem) # embeds correctly ET.indent(ET.ElementTree(elem)) # pretty-prints and re-parses
It costs one serialize + reparse — about 8 ms for a 166 KB Define-XML document.
DatasetJSONElement.to_element()raisesNotImplementedError, matching its siblingto_xml/to_xml_string/write_xmloverrides. Pinned bytests/test_xml_string_serialization.py::TestToElement.to_xml()is unchanged and is not deprecated — it remains the shared tree builder behindto_xml_string()andwrite_xml(), and existing callers keep working. It has been demoted in the documentation from the head of the serialization list to a trailing "internal tree builder" entry; preferto_element()when you want a tree.One caveat: re-serializing a
to_element()tree withET.tostring()picks prefixes from ElementTree's process-globalregister_namespace()map, so the prefix spelling may differ from the source (odm:ODMrather than a defaultxmlns). The namespaces are identical. Useto_xml_string()orwrite_xml()when exact output matters.
-
ODMElement.to_xml_string()accepts a keyword-onlyxml_declarationflag. The string path previously had no way to emit an XML declaration, so callers handing a string to a consumer that requires one had to prepend it by hand — and comparing a string against awrite_xml()file silently disagreed on the first 39 bytes. The two paths now line up exactly:odm.to_xml_string() # bytes write_xml() writes AFTER <?xml ...?> odm.to_xml_string(xml_declaration=True) # exactly what write_xml() writes
The default stays
False— this string is the documented input toODMLoader.load_odm_string()and 0.2.0 shipped it declaration-free, so flipping it would silently change output for every existing caller. The parameter is keyword-only.dataset_json_1_1.model.DatasetJSON.to_xml_string()accepts the same keyword so it still raises the intendedNotImplementedErrorrather than aTypeError. Pinned bytests/test_xml_string_serialization.py::TestXmlDeclarationOption.
write_xml()now writes LF line endings on every platform. It passed a filename toElementTree.write(), which opens the file in text mode; on Windows that translated every\nto\r\n, so the file no longer matchedto_xml_string()byte for byte and the documented equivalence between the two was false there. The writer now opens a binary handle itself. XML written on Windows changes from CRLF to LF, which makes checksums and diffs portable across platforms.
DatasetJSON.write_json(),write_ndjson(),read_json()andread_ndjson()now pinencoding="utf-8". They relied on the locale default, which is UTF-8 on Linux and macOS but cp1252 on most Windows installs — so a Dataset-JSON file containing non-ASCII text and produced by another tool could decode incorrectly or raise. Files odmlib itself writes were already ASCII-safe (json.dumpsescapes non-ASCII by default), so existing output is unaffected.
-
A nested element reached by walking a loaded tree now serializes with the namespaces its document was loaded under. The per-document namespace snapshot was bound only to the objects the loader returns directly (
root(),Study(),MetaDataVersion(),create_odmlib()). Anything reached by walking —define.Study.MetaDataVersion— had no snapshot and fell back to current global registry state, so merely importing a second Define model package changed its output:import odmlib.define_2_0.model # re-registers def: -> v2.0 globally define.to_xml_string() # root: xmlns:def=".../def/v2.1" (right) define.Study.MetaDataVersion.to_xml_string() # nested: ".../def/v2.0" (WRONG)
ns_registry.bind_document_namespaces()takes a newrecursive=Falseparameter, andloader.ODMLoader._bind_namespaces()passesrecursive=True, which covers all four loader entry points and theopen_odm/open_definecontext managers.write_xml()on a nested element is fixed by the same change. Measured cost: 1.4 ms to bind all 2 086 elements of a 136 KB Define-XML file, sharing one snapshot dict. Pinned bytests/test_xml_string_serialization.py::TestNestedElementNamespaceBinding.Residual limitation: an element constructed after the load and grafted in still carries no snapshot and uses global state. Bind it explicitly:
import odmlib.ns_registry as NS NS.bind_document_namespaces(new_elem, NS.get_document_namespaces(root))
- Emits
OdmlibDeprecationWarning; will be removed in 0.3.0. Since 0.2.1to_xml_string()declares its own namespaces, which makes this string-patching helper a no-op on any string it would normally be given. It has no callers in odmlib. Remove the call; no replacement is needed.
test_schema_ordered_serialization.py::test_to_xml_string_round_trip_unchangednever calledto_xml_string()— it used rawET.tostring(), which is false coverage of exactly the path that went untested. Renamed totest_to_xml_element_order_survives_reparse(its body is a valid element-order test) and a realto_xml_string()round-trip added beside it astest_to_xml_string_round_trip_preserves_order.
-
ARM documents can now be schema-validated against a bundled CDISC XSD. Previously odmlib shipped no ARM schema, so validating Analysis Results Metadata meant supplying your own via
xsd_file=. Two schema sets are now bundled underodmlib/schemas/arm/, registered inschema_manager._MAIN_SCHEMAand reachable through the existingODMSchemaValidatorAPI:from odmlib.odm_parser import ODMSchemaValidator validator = ODMSchemaValidator(standard="arm", version="1.0-define2.1") validator.validate_file("define-adam.xml")
(standard, version)Validates ("arm", "1.0")ARM 1.0 in a Define-XML 2.0 document (CDISC original) ("arm", "1.0-define2.1")ARM 1.0 in a Define-XML 2.1 document Two sets are required because ARM layers onto Define-XML, and Define-XML 2.0 and 2.1 use different
def:namespace URIs — the pairings are not interchangeable. Use"1.0-define2.1"withodmlib.arm_1_0, which extendsodmlib.define_2_1. The1.0-define2.1schema set is derived by odmlib from the CDISC ARM 1.0 schema by retargeting the Define-XML dependency; element and type declarations are unchanged from the original.Both ARM schemas are supersets of their base Define-XML schema, so either also validates an ARM-free Define-XML document of the matching version.
arm_1_0.model.AnalysisResultdeclared its child elements in the wrong order, so every ARM document odmlib wrote failed XSD validation. Descriptor declaration order is serialization order, and the ARM schema requires the sequenceDescription, AnalysisDatasets, Documentation, ProgrammingCode; the model declaredAnalysisDatasetslast.AnalysisDatasetshas been moved ahead ofDocumentation. Reading ARM documents was unaffected — only output was wrong, which went unnoticed while no ARM XSD was bundled to check it.
-
validate(collect_errors=True)returned at most three errors. It wrapped each of its three validation layers in a singletry/except, and every layer was itself fail-fast, so the returned list held at most one error per layer no matter how broken the document was. A document with 50 misordered elements reported 1. Each layer now enumerates every problem it finds (odm_element.py,oid_generator.py,exceptions.py):- Order: one error per misordered element; the walk now recurses into the children of a misordered element instead of aborting.
- OID: one error per duplicate OID and per bad reference. A duplicate no longer aborts the traversal, so the reference checks — which previously never ran at all once a duplicate was found — now execute. On a duplicate the first definition is kept.
- Conformance: the bundled Cerberus result is expanded into one
OdmlibConformanceErrorper failing field, each with a dottedfield_pathand the complete raw dict still oncerberus_errors.
Behaviour change:
len(errors)may now be larger than before for the same document, and errors are no longer at predictable list positions — filter by exception type rather than by index. Fail-fast mode (collect_errors=False, the default) is unchanged.
validate(max_errors=N): caps collection on a badly broken document. Validation stops the moment the cap is reached — enforced inside each layer, not by truncating afterwards — and a finalOdmlibErrorLimitErroris appended, so the list holds at mostN + 1entries. Defaults toNone(uncapped).- Collecting-checker protocol (
odmlib.exceptions): theErrorReportingmixin (report()/collecting()) and theis_collecting_checker()capability check.DynamicOIDRefimplements it; the deprecated manualOIDRefclasses and duck-typed custom checkers do not and degrade gracefully to one error for the OID layer.verify_oids()also collects when a sink is installed viawith checker.collecting(collector):. DynamicOIDRef.reset(): clears accumulated OID state so one checker can validate a second document.validate()now warns in collect mode when handed a checker that still holds state from a previous run.flatten_cerberus_errors()andOdmlibConformanceError.expand()/.field_pathfor per-field conformance reporting.ErrorCollectoracceptsmax_errorsand exposesis_full/truncated. Uncapped collectors never raise, so existing usage is unaffected.
DynamicOIDRef.check_oid_refsiterates its reference sets in sorted order. Previously which bad reference was reported first varied withPYTHONHASHSEED; error ordering is now deterministic in both modes.
- Cerberus schema isolation: each
MetadataSchemanow uses a privateSchemaRegistryinstead of the process-globalcerberus.schema_registry. Previously, instantiating checkers for two model versions (e.g. ODM 1.3.2 and Define-XML 2.1) silently corrupted each other's schemas — the last checker instantiated won for every shared schema name, rejecting valid documents and accepting invalid ones. (*/rules/metadata_schema.py) - Define-XML leaf references are now validated:
leaf/@IDdefinitions andDocumentRef/@leafID/ItemGroupDef/@def:ArchiveLocationIDreferences are surfaced to the OID checkers. A danglingleafIDpreviously passedverify_oids()silently. (odm_element.py,oid_generator.py) - Duplicate OIDs on skip-listed elements are now detected:
DynamicOIDRef.add_oidchecks uniqueness before honouringskip_elem, so duplicateItemGroupDefOIDs in Define-XML are caught (skip_elem now only exempts an element from reference-target checking). - Deprecated manual
OIDRefcrash guards:add_oid_refno longer raisesKeyErroron unregistered attributes (e.g.SignatureOID), andcheck_unreferenced_oidsno longer raisesKeyErrorfor element types missing fromdef_ref(all three model packages). - Conformance schema drift:
Presentationadded to the ODM 1.3.2MetaDataVersioncerberus schema (valid documents were rejected as "unknown field");Repeatingis nowrequiredfor StudyEventDef/FormDef/ItemGroupDef, matching the model and the spec. - Valueset lookup follows inheritance:
ValidValuesresolves theClassName.attrvalueset key via the MRO, so subclasses of model classes keep their parents' valueset validation.
- Per-document namespaces: documents loaded via
ODMLoaderremember the namespace registry state they were loaded under;write_xml()andto_xml_string()use that snapshot, so loading a second document (e.g. ODM 2.0 after ODM 1.3.2) no longer changes thexmlnsa previously loaded document serializes with. (ns_registry.py,loader.py,odm_element.py) to_xml_string()output is namespace-well-formed: it now includesxmlnsdeclarations (previously prefixed tags likedef:ValueListDefhad no declaration anywhere, so the string could not be re-parsed).set_odm_namespace_attributes_string()is a no-op on such strings.- Only used prefixes are declared: serialization emits
xmlns:entries only for prefixes actually present in the tree, so importing an unrelated model package (arm/ct/dataset) no longer pollutes output; the redundantxmlns:xmldeclaration is gone (thexmlprefix is reserved). - ARM namespace registration:
arm_1_0no longer registers itself as the default namespace (import-order dependent corruption); the registry now keeps a single default (a new default replaces the previous one), and an empty registry raisesOdmlibNamespaceErrorinstead ofIndexError.
- Security — DOCTYPE rejection: XML parsing rejects documents containing a
DOCTYPE declaration (billion-laughs / entity-expansion DoS defense) via a
cheap expat prolog pre-scan; ODM never requires DTDs. (
odm_parser.py) - Clear parse errors: malformed XML/JSON and unknown root elements now
raise
OdmlibParsingError(with hints) instead of rawParseError,JSONDecodeError, orAttributeError(all six loaders + parser). - Encoding: JSON reads use
utf-8-sig(BOM tolerant) and JSON/XML writes use UTF-8 explicitly, instead of the platform default encoding. - ODM 2.0 clinical data parsing:
ODMParser.AdminData/ClinicalData/ ReferenceDatahonour the configured namespace registry instead of a hardcoded ODM v1.3 URI (they silently returned[]for ODM 2.0 documents).
- Auto-created children no longer leak into output: reading an unset
optional child element still auto-creates it (the
rc.ErrorMessage.TranslatedText.append(...)idiom is preserved) but serialization skips auto-created elements that were never populated — read-only inspection previously injected spurious empty elements (e.g.<BasicDefinitions/>) into XML/JSON output. Reading a child whose class has required attributes returnsNoneinstead of relying on the deprecatedValueErrorbase ofOdmlibRequiredAttributeError. find/find_all/find_byguards: searching an unset single child or a scalar attribute name returnsNone/[]instead of raisingAttributeError.- Restricted subclasses are now consistent: assigning a field that a
subclass deliberately dropped (e.g.
Questionon a Define-XMLItemDef) raisesOdmlibTypeErrorinstead of silently storing a value that serialized inconsistently or not at all; constructor kwarg checking and the required-attribute check now use the class's effective field set. - Single-child type validation: an
ODMObjectdescriptor validates the items when a list is assigned (previously ANY list was accepted unchecked). - ODMBuilder scope pointers:
add_study/add_metadata_version/add_item_group_defclear stale current-element pointers, sowith_description()/with_alias()no longer attach to a previous ItemDef/MetaDataVersion after a new scope opens. - Converter dataset-name collisions:
dataset_xml_to_dataset_jsonwarns and keys a colliding dataset by its fullItemGroupOIDinstead of silently overwriting (e.g.IG.AEvsSUPP.AEboth deriving "AE"). - DataFrame row drops are visible:
dataframe_to_itemswarns (with row index and reason) for rows that fail element construction instead of silently returning fewer elements.
merge_fields=Trueclass keyword for model subclassing: a subclass declared asclass MyItemDef(ODM.ItemDef, merge_fields=True)inherits all base-class fields/elements without redeclaring them (redeclaring a field moves it to the subclass position). The default remains the historical declare-from-scratch behaviour that the Define-XML models use to restrict inherited ODM fields. (ODMMeta)- Context managers are read-only by default (breaking):
open_odm()/open_define()without anoutput_fileno longer rewrite the input file on exit. Writing requires an explicitoutput_file, orwrite_on_exit=Trueto opt in to an in-place update.
- Format-validation regexes (datetime/partial/incomplete/SAS names) are compiled once at import instead of on every attribute assignment.
- Compiled XML schemas are cached by path (
ODMSchemaValidatorno longer recompiles the XSD per instance); cerberusValidatorobjects are cached perMetadataSchemainstance. - Removed no-op filter-dict allocations from the
to_dict/OID/order traversals; Define manual checkers setis_verifiedsounreferenced_oids()no longer re-runs the full verification walk;dataframe.pyavoidsiterrows.
v0.2.0 shipped with five ODM 2.0 structural features that produced schema-invalid output
if used, listed under Known Limitations in that release as deferred to v0.2.1. All five
are closed. tests/test_odm_2_0_known_gaps.py kept its assertions as regression guards
after the xfail(strict=True) markers came off, so none of them can quietly reopen.
ConditionDefgained the XSD-requiredMethodSignature, and itsDescriptionbecame required. EveryConditionDefodmlib built was previously schema-invalid, and so transitively was anyCollectionExceptionConditionOIDpointing at one.FormalExpressionbecame element-based. See the breaking change below.Protocolno longer carriesStudyEventRef. The ODM 2.0 XSD reaches study events throughStudyEventGroupRef→StudyEventGroupDef, soProtocolgainedStudyEventGroupRef,StudyTimingsandWorkflowRefinstead.MetaDataVersion.StudyTimingmoved toProtocol/StudyTimings, its XSD position. A newStudyTimingscontainer holds theStudyTimingelements, which makes the four timing-constraint classes reachable in a valid document for the first time.StudyEventGroupDefgained its required child group —StudyEventGroupRefandStudyEventRef, plus the optionalWorkflowRefandCoding. It previously could not satisfy its own content model at all.
Breaking within draft ODM 2.0: FormalExpression no longer takes _content. The XSD
models the expression as a choice of exactly one Code (inline source) or
ExternalCodeLib (a reference to an external library), not as element text. Code that
constructed FormalExpression(Context=..., _content="...") under model_package="odm_2_0"
now raises OdmlibTypeError; the expression text moves into a Code child:
# before (schema-invalid)
FormalExpression(Context="Python", _content="age >= 18")
# after
FormalExpression(Context="Python", Code=Code(_content="age >= 18"))ODM 1.3.2 and Define-XML FormalExpression are unchanged and remain text-based.
ODMBuilder.add_method_def(formal_expression=...) and add_condition_def(...) still take
plain text and wrap it correctly for whichever model package is in use, so builder callers
need no change.
Two follow-on gaps found while verifying the five also closed: CommentDef and Leaf
became MetaDataVersion children — both classes already existed but were unreachable from
a document, so a CommentOID could never resolve — and DocumentRef's leafID attribute
was corrected to LeafID, the spelling the XSD requires.
Comparing odmlib/odm_2_0/model.py against the bundled ODM 2.0 XSD mechanically —
rather than by hand, one document at a time — surfaced far more divergence than the
structural gaps closed earlier in this release. tests/test_odm_2_0_xsd_alignment.py
now pins the whole divergence set against an allowlist, so drift cannot be introduced
or silently fixed without the test failing. ODM_XSD_ALIGNMENT.md plans the rest.
This first pass corrects the members that made odmlib's own output schema-invalid.
These are breaking changes within draft ODM 2.0. There is no alias layer in odmlib —
a descriptor name is the XML attribute name — so a document using an old spelling now
raises OdmlibTypeError on load in strict mode, and loses the value silently in
permissive mode. That is the intended outcome: none of the old spellings were ever valid
against the ODM 2.0 schema. ODM 1.3.2, Define-XML, ARM, CT and Dataset-XML/JSON are
untouched.
Telecom.value→Telecom.Value.TelecomAttributeDefinitioncapitalises it; the lower-case spelling made every document containing aTelecomschema-invalid. Same class of bug as theDocumentRefleafID→LeafIDfix.ODM.Archivalremoved. It is not inODMAttributeDefinition. TheODM.Archivalvalue-set key went with it.User.Prefix/User.Suffixare now child elements, matching the XSD, and newPrefix/Suffixclasses back them.User.DisplayNameremoved — not part of the ODM 2.0 XSD — along with theDisplayNameclass.Organizationreplaced. The text-only leaf carried over from ODM 1.3.2 was orphaned — referenced by nothing — and has been replaced by the element the XSD defines:OID/Name/Typerequired, plusRole,LocationOID,PartOfOrganizationOIDandDescription/Address/Telecomchildren. It is now anAdminDatachild in its own right; aUserlinks to one throughOrganizationOID, which consequently resolves underverify_oids()for the first time.RelativeTimingConstraint: the fourPredecessor*/Successor*OID attributes were replaced by the XSD'sPredecessorOIDandSuccessorOID. Either may reference any structural element, so splitting them by event-vs-group was both wrong and unnecessary.oid_generator_config.pyskips the two new names in their place.TransitionTimingConstraint:TimepointRelativeTarget→TimepointTarget, and the missingTypeattribute was added.Originchildren reordered to the XSD sequence —Description,SourceItems,DocumentRef. odmlib serializes in declaration order, so the old order emittedDocumentReffirst and the document failed validation. This changes serialized output for anyOriginthat carries aDocumentRef.WorkflowEndgained_content. Its XSD type isxs:simpleContentovertextand the model had no text member at all.DurationDateTimeStringaccepts the wholedurationDatetimeunion. It enforced[+-]P{n}Wonly, rejecting XSD-valid values such asP3D,PT1H30Mand the empty tag. The descriptor is used solely by the four ODM 2.0 timing-constraint classes, so no other standard is affected.DocumentRef/@LeafIDis ref-checked again. ID-based reference detection is hardcoded string sniffing, and neither the attribute name nor the lower-caseleaftarget ever matchedodm_2_0.LeafIDnow resolves toLeaf, so a dangling document reference raises like any other unresolved OID.- New value-set keys for
Organization.TypeandTransitionTimingConstraint.Type.
Required flags and cardinalities brought into line with the ODM 2.0 XSD. Most of this
is declarative — required on a child-element descriptor is not enforced at
construction — but three groups do change behaviour.
Attributes the model demanded that the XSD marks optional no longer raise when
omitted: FormalExpression.Context, MethodDef.Type, TargetTransition.ConditionOID,
and the pre/post window attributes on all four timing constraints
(AbsoluteTimingConstraint, RelativeTimingConstraint, TransitionTimingConstraint,
DurationTimingConstraint). This is a pure loosening; existing code that supplies them
is unaffected.
Standard.Status is now required, matching use="required" in the XSD. Unlike the
element-level tightenings this is enforced at construction, so
Standard(OID=…, Name=…, Type=…, Version=…) without a Status now raises
OdmlibRequiredAttributeError. Leaf.Title, MethodDef.MethodSignature and
Study.MetaDataVersion were also marked required, but as child elements those are
declarative only.
Cardinality corrections change the shape of four attributes. Code that indexed or appended to them needs updating:
- Now single (
maxOccurs="1"in the XSD):MetaDataVersion.Standards,Address.StreetName, andWorkflowRefonItemGroupDef,StudyEventDefandStudyStructure. Note the severalStandardentries live inside the oneStandardscontainer, so nothing is lost. - Now lists (
maxOccurs="unbounded"):AnnotatedCRF.DocumentRef,SupplementalDoc.DocumentRefandStudyTiming.TransitionTimingConstraint. Each could previously hold only one reference where the schema allows many.
Assigning a list to a now-single child is still tolerated by ODMObject.__set__ and
serializes every item, so odmlib does not reject over-long content itself — XSD
validation is what catches it.
Members the ODM 2.0 XSD defines but the model never had. All additive — no existing attribute changed name, type or cardinality.
New classes: Class and SubClass (ODM 2.0 models a dataset's general observation
class as a child element with a Name attribute, not the Define-XML-style
ItemGroupDef/@Class attribute), ValueListRef, HouseNumber and GeoPosition.
New attributes: ItemGroupDef.Structure and .ArchiveLocationID,
ItemGroupRef.MethodOID, CodeListItem.ExtendedValue, Include.href,
PDFPageRef.Title, RangeCheck.ItemOID, Location.OrganizationOID.
New children: MetaDataVersion gains AnnotatedCRF, SupplementalDoc,
ValueListDef and WhereClauseDef — all four classes already existed but were
unreachable from a document, the same gap CommentDef and Leaf had. ItemDef gains
ValueListRef; ItemGroupDef gains Class and Leaf; MethodDef gains DocumentRef;
RangeCheck gains MethodSignature; Origin, SourceItems and StudyEventDef gain
Coding; Location gains Description, Address and Telecom; Address gains
HouseNumber and GeoPosition.
Value sets: ItemGroupDef.Class was replaced by Class.Name, and SubClass.Name
and SubClass.ParentClass added. The dead Location.LocationType key was removed — the
model attribute has been Role (free text) for some time, so that key matched nothing
and left Role unvalidated. CodeListItem.ExtendedValue was already present and starts
resolving now that the attribute exists.
Parameter, ReturnValue and MethodSignature moved earlier in model.py so that
RangeCheck can reference MethodSignature. Class order in the module carries no
meaning beyond definition-before-use.
Note for anyone extending these classes. Several additions insert a descriptor in the
middle of a class body, which is how child order is expressed. verify_order() reads
each class's own body, so a subclass of an odm_2_0 model class that does not opt into
merge_fields=True must redeclare inherited children in the new order. No shipped model
uses merge_fields, so this affects third-party extensions only.
The Protocol study-design subtree, deliberately left unmodelled when Protocol was
first aligned earlier in this release. Twenty-four new classes fill the nine optional
child slots the XSD defines, in XSD sequence order:
StudySummary→StudyParameter→ParameterValue— named summary parameters such as trial blinding schema.TrialPhase— the trial phase, value-set checked against the 13 XSD terms.StudyIndications→StudyIndication, andStudyInterventions→StudyIntervention(withStudyInterventionRef).StudyObjectives→StudyObjective, andStudyEndPoints→StudyEndPoint(withStudyEndPointRef), so an objective can point at the endpoints assessing it.StudyTargetPopulation(withStudyTargetPopulationRef).StudyEstimands→StudyEstimand→IntercurrentEvent/SummaryMeasure— the ICH E9(R1) estimand framework, which ODM 2.0 models natively.InclusionExclusionCriteria→InclusionCriteria/ExclusionCriteria→Criterion, each criterion pointing at aConditionDef.
All nine Protocol children are minOccurs="0", so existing documents are unaffected.
Value sets: TrialPhase.Value and StudyEndPoint.Level added. StudyObjective.Leve
— a truncated key that matched nothing, leaving StudyObjective.Level unvalidated — was
corrected to StudyObjective.Level. StudyEndPoint.Type and StudyEstimand.Level were
already present and start resolving now that their classes exist.
With this phase every ODM 2.0 metadata element the XSD defines is modelled. The 24 XSD
elements still without a class are the ClinicalData/ReferenceData data layer, which
ROADMAP.md books for v0.3.0.
The final alignment phase: record what odmlib deliberately does not express, and remove what the ODM 2.0 XSD does not define. Every model class now corresponds to an ODM 2.0 XSD element, and every metadata element the XSD defines is modelled.
Removed seven ODM 1.3.2 carry-over classes that have no ODM 2.0 XSD element:
ArchiveLayout, Email, ExceptionEvent, Fax, Pager, Phone and Picture
(DisplayName went in phase 1). None was reachable from ODM — no descriptor anywhere
in the model referenced them — so nothing can be lost from a document; they could only
be constructed in isolation and never attached. ODM 2.0 folds Email/Fax/Pager/
Phone into Telecom with a TelecomType of the same name, and replaces Picture
with Image.
Three approximations are now documented rather than latent, each with a
.. note:: Known approximation in its class docstring, in Known Limitations in
README.md, and in the ODM 2.0 section of the model reference guide. In each case
odmlib's descriptor model cannot express what the XSD says, so
ODMSchemaValidator — not object construction — is what catches a violation:
FormalExpression— the XSD requires exactly one ofCodeorExternalCodeLib. odmlib has no way to express anxs:choice, so both are optional; setting neither, or both, builds an object odmlib accepts and the schema rejects.odm_2_0has no Cerberus rules package to enforce it in either.StudyEventGroupDef— the XSD's repeating(StudyEventGroupRef?, StudyEventRef?)group permits the two to interleave. Two parallel lists emit all groups then all events. Reading is affected too: the loader collects children by tag, so an interleaved source document loads correctly and re-serializes grouped. Both forms are schema-valid; only the ordering is lost.TranslatedText— the XSD types itmixed="true"with an optionalxhtml:divchild, so text may carry XHTML markup. odmlib models the text-only form and drops anxhtml:divon load. Faithful support would need a model class for every elementODM-xhtml.xsdallows, since the loader resolves children by tag name; and odmlib never reads ElementTree'stail, so text following a child element is lost regardless. Plain-textTranslatedTextround-trips exactly.
model.pyi was brought into line at the same time: the eight stub-only classes naming
elements the model has never had were removed, and CodeList, User, UserName,
GivenName, FamilyName and Image were regenerated or added so that no stub
annotation refers to an undefined class.
The odm_2_0 block of odmlib/data/valuesets.json was wrong in both directions. Checking
it against what each attribute's XSD type actually permits — rather than only asking which
keys resolve — turned up eight defects that no test caught.
odmlib rejected schema-valid values in three places. ItemGroupDef.Type and
TrialPhase.Value have XSD types that union an enumeration with bare xs:string, making
them extensible vocabularies, but were enforced as closed lists, so a sponsor-specific
value raised. ODM.ODMVersion is a pattern admitting 2.0.1 and 2.0-draft, stored as
the single literal "2.0".
Two lists held the wrong values. MethodDef.Type accepted Other, which ODM 2.0 does
not define, and rejected Preload, which it does. User.UserType offered four values
where ODM 2.0 has nine, rejecting Subject, Monitor, Data analyst, Care provider
and Assessor. Both had been copied from the odm_1_3_2 block — which is correct for ODM
1.3.2; ODM 2.0 changed both enumerations and the copy never caught up.
Standard was not value-checked at all. Name, Type and PublishingSet are closed
enumerations in the XSD but were modelled as plain strings, so Standard(Type="Nonsense")
built without complaint on a reachable, mostly-required element. They are now
ValueSetString. Standard.Status is an extensible union and takes the open form instead.
Nine keys matching no descriptor were removed, four of them misspellings of attribute names
on ClinicalData classes that arrive in v0.3.0 (AuditRecord.EditPoin,
Comment.SponsorOrSit, Query.SourceSyste, Query.Status). They were not corrected and
kept: a key for a class that does not exist cannot be verified, which is how the
misspellings survived in the first place. v0.3.0 adds them with their classes.
New in odmlib/valueset.py: a third entry form for extensible vocabularies,
{"_values": [...], "_open": true}, alongside the existing list and _regex forms. Any
value is accepted; the listed terms remain the documented ones and drive describe().
Guard. tests/test_odm_2_0_xsd_alignment.py compared value-set key names in both
directions and never looked at values, which is why this survived four alignment phases.
It now classifies each attribute's XSD type as closed enumeration, extensible union,
pattern or free, and reports a missing key, wrong values, a closed list where the schema is
open, or an open entry where the schema is closed. All three value-set allowlists are empty.
A note on the two directions, since several code comments had it backwards: a value-set key
with no descriptor is inert — unused data. A ValueSetString descriptor with no key is
the loud one: validate() maps the unknown sentinel to False and the descriptor then
raises for every value, so the attribute cannot be set at all.
0.2.0- 2026-06-23
odmlib/context.py: addedwrite_on_exit: bool = Trueparameter toODMContext,DefineContext,open_odm, andopen_define. Passingwrite_on_exit=Falsesuppresses the auto-save on clean exit, enabling read-only inspection through the context managers without modifying or creating any file. The default (True) preserves the documented in-place save behaviour — additive change, no compat impact.tests/test_context_managers.py: 10 new tests covering the opt-out (XML, JSON,open_odm,open_define, and the input-preservation regression guard for the default-output-file footgun) plus explicit default-still-writes guards.
odmlib/data/valuesets.json: added 12 missingodm_2_0value-set keys (ReturnValue.DataType,ItemRef.Core/Repeat/Other/IsNonStandard/HasNoData,CodeListItem.Other,Telecom.TelecomType,ItemGroupDef.IsNonStandard/HasNoData,CodeList.IsNonStandard,ODM.Context) bound to the ODM 2.0ODM-enumerations.xsdvalue lists.tests/test_odm_2_0_model.py: new construction + XML/JSON round-trip suite for the major ODM 2.0 classes.tests/test_odm_2_0_known_gaps.py: new strict-xfailmarkers pinning the five deferred structural ODM 2.0 model/XSD gaps so CI documents the known state and fails loudly if a gap is silently fixed or regressed. (Its ItemDef test is now a passing regression guard — see Changed below.)
-
ODM 2.0
TranslatedText.Typeis now required (odmlib/odm_2_0/model.py), matching the XSD (use="required", free-text media type).ODMBuildernow defaultsType="text/plain"for the ODM 2.0 model shape only via a new_translated_text()helper. -
odmlib/valueset.py:ValueSet.value_set()no longer raises for an unknown attribute — it returns the newValueSet.UNKNOWN_ATTRIBUTEsentinel sovalidate()returnsFalseand theSKIP_VALUESETpermissive guard can bypass an unregistered value set. Unknown version still raises. Behavioral note: in strict mode an unregistered value-set attribute now raisesOdmlibTypeError(fromValidValues.__set__) instead of the formerOdmlibValidationError(fromvalue_set()). -
ODM v2.0
ItemDefaligned with the ODM 2.0 XSD (odmlib/odm_2_0/model.py). Removed the XSD-rejected attributesFractionDigits,DatasetVarName, andSDSVarName; added the XSD-defined optional attributesDisplayFormatandVariableSet. Code that set the removed attributes on anodm_2_0ItemDefshould migrate — those values were schema-invalid and are no longer serialized. (Closes the ROADMAP v0.2.1 ItemDef gap /ODM20-MODEL-XSD-DIFFERENCES_PLAN.md§3.7; the XSDItemDef/ValueListRefchild element remains deferred.)
- ODM v2.0 structural model/XSD gaps deferred to v0.2.1. Five features
produce schema-invalid output if used under
model_package="odm_2_0":ConditionDef(no requiredMethodSignature), text-basedFormalExpression,Protocol.StudyEventRef(removed in the 2.0 schema),MetaDataVersion.StudyTimingplacement, andStudyEventGroupDef(missing required child group). The affectedODMBuilderhelpers carry docstring caveats. See ROADMAP "v0.2.1 — ODM v2.0 Model/XSD Alignment" andODM20-MODEL-XSD-DIFFERENCES_PLAN.md.
odmlib/schema_manager.py: registered("odm", "2.0") → "ODM.xsd"in_MAIN_SCHEMA.ODMSchemaValidator(standard="odm", version="2.0")now resolves the bundledodmlib/schemas/odm/2.0/ODM.xsd(target namespacehttp://www.cdisc.org/ns/odm/v2.0) and exposes the samevalidate_tree()/validate_file()API used for ODM 1.3.2 and Define-XML.- The v2.0 XSD set (
ODM.xsd+ODM-foundation.xsd+ 7 modular includes + xlink/xml/xhtml) was already shipping inodmlib/schemas/odm/2.0/via theschemas/**/*.xsdpackage-data glob; the registry entry is the only missing wiring. tests/test_schema_manager.py: 4 new tests verifyingget_schema_dir("odm", "2.0"),get_schema_path("odm", "2.0"), the resolved filename (ODM.xsd), and the integration file-existence check.tests/test_odm_validator.py: newTestODMv20Validatorclass validatingtests/data/odmv2_example.xmlandtests/data/cdash_demo_v20.xmlend to end, plus a regression test that an ODM 1.3.2 document fails v2.0 validation. Newtest_explicit_odm_v20_works()inTestODMValidatorConstructorContract.docs/source/guides/validation.rst: rewrote the "Schema Validation" section to documentODMSchemaValidator(the previous text referenced a nonexistentSchemaManagerclass).
- New
odmlib/mode.pymodule withValidationModeflag enum andpermissive()context manager for loading non-conformant ODM documents ValidationMode.STRICT(default) — all validation enforced (existing behavior, unchanged)ValidationMode.SKIP_REQUIRED— omit required-attribute checks during construction and accessValidationMode.SKIP_TYPE— omit type checks (Typed, Integer, Float, ODMObject, ODMListObject, Positive, NonNegative, and unknown-attribute rejection)ValidationMode.SKIP_FORMAT— omit format validators (datetime, SAS name/format, email, URL, filename, regex, sized string)ValidationMode.SKIP_VALUESET— omit ValidValues and ExtendedValidValues enforcementValidationMode.PERMISSIVE— composite flag that skips all validation categoriespermissive()context manager with automatic cleanup viacontextvars.ContextVar; supports graduated control via flag combinationsopen_odm()andopen_define()context managers acceptpermissive=Trueor a specificValidationModecombinationValidationMode,permissive,get_mode,set_modeexported fromodmlibpackage root- New how-to guide:
docs/source/guides/permissive_loading.rst tests/test_permissive_mode.py— 70 tests covering all validation categories, context manager safety, integration with loaders, and the load-fix-validate workflow
- New
odmlib/exceptions.pymodule with a structured exception hierarchy OdmlibError— base class for all odmlib exceptionsOdmlibValidationError— replaces bareValueErrorfor validation failures; includeselement_path,hint,attribute,element_type, andactual_valueattributesOdmlibRequiredAttributeError— raised when a required attribute is missing at constructionOdmlibOIDError— raised for OID uniqueness or ref/def integrity failuresOdmlibConformanceError— raised when Cerberus conformance validation fails; exposes rawcerberus_errorsdict for programmatic inspectionOdmlibElementOrderError— raised when child elements violate ODM-spec orderingOdmlibTypeError— replaces bareTypeErrorfor type/enum validation failuresOdmlibParsingError— raised when an XML or JSON document cannot be parsedOdmlibLoaderStateError— raised when a loader method is called before the document is openedOdmlibSerializationError— raised when the model cannot be serialized to XML or JSONOdmlibNamespaceError— raised for namespace registration or lookup failuresOdmlibWarning,OdmlibDeprecationWarning,OdmlibInteroperabilityWarning— warning hierarchyErrorCollector— accumulates validation errors instead of raising on the first failure;has_errors,add_error(),add_warning(),raise_if_errors()APIODMElement.validate()method — unified validation entry point supporting both fail-fast (default) and collect-all-errors (collect_errors=True) modes- All exceptions and
ErrorCollectorexported fromodmlibpackage root tests/test_exceptions.py— 47 tests covering hierarchy, formatting, backward compat, and model integrationtests/test_collect_errors.py— 10 tests covering fail-fast and collect-all-errors validation modes
- New
odmlib/dataset_json_1_1/package — spec-conformant Dataset-JSON v1.1 support using the ODMElement/descriptor pattern (one dataset per file, matching the v1.1 specification)DatasetJSON— root element withto_json(),from_json(),write_json(),read_json(),write_ndjson(),read_ndjson(),to_dict(),from_dict(),add_row(),add_column(),column_namespropertyColumn— column metadata with validateddataTypeand optionaltargetDataType,length,displayFormat,keySequenceSourceSystem— optional nested source system metadata objectDatasetJSONElementbase class disables XML serialization (to_xml()raisesNotImplementedError) and handles mixed list types into_dict()
- New
odmlib/dataset_json_1_1/define_flattener.py— converts Define-XML v2.1 metadata into 11 tabular Dataset-JSON datasets (study, standards, datasets, variables, value_level, where_clauses, methods, comments, documents, codelists, codelist_terms)DefineFlattener.flatten_all()returns dict of dataset name → DatasetJSONDefineFlattener.write_all(output_dir)writes individual JSON files- O(1) ItemDef and WhereClauseDef lookups via index
_safe_get()utility for traversing deeply nested optional attributes
- New
odmlib/dataset_json_1_1/converter.py— bidirectional Dataset-XML ↔ Dataset-JSON v1.1dataset_xml_to_dataset_json(odm_obj)returns dict[str, DatasetJSON] (one per ItemGroupOID)dataset_json_to_dataset_xml(dataset_json, model)converts back to Dataset-XML
- Updated
odmlib/dataframe.py— Pandas integration for the new modeldataset_json_to_dataframe()— DatasetJSON v1.1 → DataFramedefine_metadata_to_dataframes(odm_root)— Define-XML v2.1 → dict of DataFramesdataframe_to_dataset_json(df, name, label, oid)— DataFrame → DatasetJSON v1.1
DatasetJSON,Column,SourceSystem,DefineFlattener,dataset_xml_to_dataset_json,dataset_json_to_dataset_xmlexported fromodmlibpackage root- New how-to guide:
docs/source/guides/dataset_json.rst - New API reference page:
docs/source/odmlib.dataset_json_1_1.rst - New tests:
tests/test_dataset_json_1_1_model.py(68 tests),tests/test_define_flattener.py(45 tests),tests/test_dataset_json_1_1_converter.py(21 tests),tests/test_dataframe_phase3.py(39 tests)
- New
odmlib/dataframe.py— optional Pandas DataFrame integrationmetadata_to_dataframe(mdv, element_type, attributes=None)— exports odmlib metadata elements (ItemDef, ItemGroupDef, CodeList, etc.) as a DataFrameclinical_data_to_dataframe(clinical_data, item_group_oid)— flattens ODM 1.3.2 hierarchical clinical data (SubjectData → StudyEventData → FormData → ItemGroupData) into a tabular DataFramedataset_to_dataframe(clinical_data)— flattens Dataset-XML 1.0.1 ClinicalData (flat structure, no SubjectData) into a DataFramedataframe_to_items(df, model_module, element_type, column_mapping=None)— creates odmlib element instances from a DataFrame (one row per element); skips invalid rows- Graceful degradation: importing the module always succeeds; calling any function
raises
ImportErrorwith install hint when pandas is absent
- Optional dependency group
dataframeadded topyproject.toml:pip install odmlib[dataframe]installs pandas ≥ 1.5 - pandas ≥ 1.5 added to the
devdependency group so the full test suite runs against pandas in CI development environments - New
tests/test_dataframe.py— tests for DataFrame integration (skipped automatically when pandas is not installed) - New how-to guide:
docs/source/guides/interoperability.rst - New API reference page:
odmlib.dataframeadded to Sphinxindex.rst
pyproject.tomlfor modern Python packaging (replacessetup.pyas primary config)- GitHub Actions CI: automated testing on Python 3.9–3.13
- GitHub Actions: automated PyPI publishing on tagged releases
CHANGELOG.mdfor tracking changes going forward- Semantic versioning policy starting at 0.2.0
OdmlibSchemaValidationErrornow inherits fromOdmlibValidationError(and therefore fromOdmlibError). Previously it inherited only fromExceptionand was excluded from the unified hierarchy. A singleexcept OdmlibValidationErrornow catches both XSD violations raised byODMSchemaValidator.validate_file()and in-memory model validation failures (required-attr, OID, conformance, element order).- The class has moved from
odmlib/odm_parser.pytoodmlib/exceptions.py.from odmlib.odm_parser import OdmlibSchemaValidationErrorcontinues to work via re-export — no migration required for existing callers. - Also exported from the package root:
from odmlib import OdmlibSchemaValidationError. - Backward compatibility:
ex.args[0]still returns the wrappedxmlschemaexception, so callers usingex.args[0].msgare unaffected. The wrapped exception is also accessible via the newex.wrappedattribute.
- Bumped minimum supported Python version from 3.9 to 3.10. Python 3.9
reached end-of-life in October 2025 and is no longer tested in CI.
Users on 3.9 should upgrade; install requires
requires-python = ">=3.10". - CI matrix now tests Python 3.10, 3.11, 3.12, and 3.13.
- Workflow hardening:
fail-fast: false(all matrix versions report independently), action versions bumped to Node 24-compatible releases (actions/checkout@v5,actions/setup-python@v6,peaceiris/actions-gh-pages@v4), least-privilegeGITHUB_TOKENpermissions, and a concurrency group so superseded runs on the same ref are cancelled.
odmlib/descriptor.py:Descriptor.__get__returnsNonefor unset required attributes whenSKIP_REQUIREDmode is active (previously always raisedOdmlibRequiredAttributeError)odmlib/odm_element.py:ODMElement.__init__and__setattr__bypass unknown-attribute rejection and required-attribute enforcement when appropriate mode flags are activeodmlib/typed.py: all 25__set__methods check the currentValidationModebefore raising validation exceptionsodmlib/context.py:open_odm()andopen_define()accept a newpermissiveparameter;ODMContextandDefineContextmanage mode lifecycle in__enter__/__exit__
- All ~70
ValueError/TypeErrorraises across 16 files replaced with structured odmlib exceptions odmlib/odm_element.py:verify_order()now raisesOdmlibElementOrderErrorwith a hint to usereorder_object();reorder_object()issues anOdmlibWarningbefore silently reorderingodmlib/odm_1_3_2,odmlib/define_2_0,odmlib/define_2_1conformance checkers now raiseOdmlibConformanceErrorwith structuredcerberus_errorsattribute instead ofValueError(dict)
- New
odmlib/oid_generator.pymodule with fully dynamic OID ref/def checking derived from model class introspection — eliminates manual maintenance ofoid_ref.pyfiles DynamicOIDRefclass — drop-in replacement for the manualOIDRefclasses with the sameadd_oid(),add_oid_ref(),check_oid_refs(), andcheck_unreferenced_oids()APIcreate_oid_checker(model_package, extra_skip_attrs=None, extra_skip_elems=None)factory function — primary public API for creating OID checkers; exported fromodmlibpackage rootodmlib/oid_generator_config.py— per-model skip-attribute and skip-element configuration- ODM 2.0 OID checking now supported for the first time via
create_oid_checker("odm_2_0") - Manual
OIDRefclasses inrules/oid_ref.pyfor all three model packages (odm_1_3_2,define_2_0,define_2_1) now emitOdmlibDeprecationWarningon instantiation; they remain functional and will be removed in v0.3.0 tests/test_oid_generator.py— 57 new tests covering model introspection, mapping correctness, DynamicOIDRef behavior, end-to-end validation, and deprecation warnings
- New
odmlib/arm_1_0/package — CDISC Analysis Results Metadata (ARM) v1.0 model using the ODMElement/descriptor pattern- Supports ARM elements:
AnalysisResultDisplays,ResultDisplay,AnalysisResult,AnalysisDatasets,AnalysisDataset,AnalysisVariable,ProgrammingCode,Code,Documentation,AnalysisDocumentation, and supporting elements - Integrates with Define-XML 2.1 for analysis results metadata
- ARM namespace (
arm) registered automatically on import
- Supports ARM elements:
odmlib/valueset.py:ValueSet.validate(value)— validates a value against the valueset's allowed values, including regex pattern matching for string-type entriesodmlib/valueset.py:ValueSet.describe()— returns a human-readable description of the valid values for error messagesodmlib/data/valuesets.json:MetaDataVersion.DefineVersionconverted from enumerated list to regex pattern^2\.[01](\.\d+)?$for flexible version matchingtests/test_valueset.py— 30 tests for regex validation and describe functionality
ODMElement.find_all(element_type, attribute, value)— find all matching child elements in a list attributeODMElement.find_by(**kwargs)— find a child element matching multiple attribute criteria
- New
odmlib/builder.py—ODMBuilderclass providing a fluent/chained API for building ODM documents programmatically withadd_study(),add_metadata_version(),add_item_group_def(),add_item_def(),add_code_list(), andbuild()methods
- Unified version to
0.2.0acrosspyproject.toml,odmlib/__init__.py, anddocs/source/conf.py odmlib/__version__now read dynamically from installed package metadata viaimportlib.metadata- Installation:
pip install -e ".[dev]"replacespython setup.py develop - Installation:
pip install -e .replacespython setup.py install requirements.txtaligned to matchpyproject.tomldependency minimum versions
odmlib/odm_parser.py:ODMSchemaValidator.__init__no longer silently defaults tostandard="odm",version="1.3.2". The signature is now(xsd_file=None, standard: Optional[str] = None, version: Optional[str] = None). Callers must provide either anxsd_filepath, or bothstandardandversion; otherwise aValueErroris raised at construction time. The silent ODM 1.3.2 fallback was a footgun — for example, a Define-XML 2.1 document could be validated against the ODM 1.3.2 schema without any warning. Existing call sites that already passstandard=andversion=explicitly are unaffected. TheValueErrorhint explicitly points users with custom or local schemas (anything not inschema_manager._MAIN_SCHEMA) at thexsd_file=<path>escape hatch, so they don't accidentally fall back to a packaged schema lookup that doesn't apply to them.tests/test_odm_validator.py: newTestODMValidatorConstructorContractclass with 8 tests locking in the new error contract (no-args raises, hint mentionsxsd_file=, partial-args raises, both-args works, xsd_file works, xsd_file precedence, custom out-of-tree xsd works).
odmlib/odm_element.py:ODMElement.to_xml()now emits child elements in model declaration order (driven by_elems) instead of attribute insertion order (self.__dict__). Previously, when a user assigned child attributes in an order that diverged from the schema declaration — for example mutatingigd.Description.TranslatedTextafter the list-typedItemRefhad already been pre-populated by__init__, or assigningigd.Classlast — the saved XML put<Description>after the<ItemRef>block and<def:Class>after<ItemRef>but before<def:leaf>only by coincidence of the__dict__ordering. Define-XML 2.1 XSD validation rejected such files.- The fix matches what
verify_order()andreorder_object()already trusted: declaration order from the class body, captured byODMMetainto_elems. Users no longer need to callreorder_object()before serializing — assignment order is fully decoupled from emission order. tests/test_schema_ordered_serialization.py— 7 tests pinning the new behaviour: ItemDef/ItemGroupDef children come out in schema order regardless of assignment order; the Define-XML 2.1 ItemGroupDef pattern fromnotebooks/first_define.ipynb(Description→ItemRef*→def:Class→def:leaf) is locked in; unset optional children are silently skipped; attribute serialization and_contentemission are unchanged.
XMLDefineLoadernamespace mismatch: the defaultns_uriwas set to a Define-XML 2.0 default regardless ofmodel_package, causingdef:-namespaced child ofMetaDataVersion(Standard,CommentDef,ValueListDef,WhereClauseDef,leaf, …) to be dropped when loading Define-XML 2.1 documents (when no ns_uri was provided). Thens_uriargument is nowOptional[str]and, when omitted, derived frommodel_package(define_2_0→…/v2.0,define_2_1→…/v2.1). Explicit values still override the derived default.XMLODMLoaderns_uriparameter was dead code: the constructor accepted anns_uriargument but_set_namespaceused the default ODM 1.3 URI. The parameter is now stored on the loader and used by_set_namespace; default is derived frommodel_package(odm_1_3_2→…/v1.3,odm_2_0→…/v2.0).XMLArmLoader._set_registrydocumented in-place: ARM 1.0 is intentionally paired with ODM 1.3 and Define-XML 2.1, no behavior change.- New regression suite:
tests/test_loader_ns_defaults.py— 12 tests covering Define 2.0/2.1 derivation, ODM 1.3.2/2.0 derivation, explicit override behavior, fallback for unknownmodel_package, and the end-to-end Define-XML 2.1 OID-index regression that reproduced the original bug report. XMLODMLoader.__init__no longer mutates the globalNamespaceRegistryBorg singleton at construction time. Pre-fix,__init__unconditionally called_set_namespace(None), which registeredodm → self.nos_uriimmediately. When users passed a non-canonical wrapper URI (e.g.library-xml/v1.0for the CDISC Library CDASH endpoint), the canonicalodm → http://www.cdisc.org/ns/odm/v1.3mapping was silently overwritten in the global Borg, breaking any code in the same process that depended on it.XMLODMLoader.__init__now mirrorsXMLDefineLoader.__init__: it storesself.ns_uribut assigns an emptyNamespaceRegistry()view toself.nsrand defers the actual prefix registration tocreate_document/create_document_from_string.create_documentwas also updated to call_set_namespace(namespace_registry)unconditionally so that the deferred registration still happens when no caller-supplied registry is provided. Thensr=constructor argument continues to be honored immediately.tests/test_loader_ns_defaults.py: addedTestODMLoaderConstructionDoesNotMutateBorg(4 tests) covering the no-mutation contract, the canonical-URI symmetry case, the explicitnsr=constructor argument, and deferred-registration-at-parse-time; updatedtest_odm_explicit_ns_uri_now_takes_effectto trigger the deferred registration before asserting onloader.nsr.
- Trailing space bugs in
odmlib/odm_1_3_2/rules/oid_ref.py_init_def_ref():"SignatureOID "corrected to"SignatureOID"and"ItemOID "corrected to"ItemOID"; these would have caused silent failures incheck_unreferenced_oids()
MANIFEST.intypo:test/datacorrected totests/data- Version inconsistency: was 0.1.4 (
setup.py), 0.1.2 (__init__.py), 0.1.0 (docs/) - Missing
odmlib.odm_2_0package in build configuration (now auto-discovered viapackages.find)
In v0.2.x, odmlib validation and type exceptions dual-inherit from ValueError/TypeError so that
all existing except ValueError and except TypeError clauses continue to work unchanged.
v0.3.0 breaking change: The ValueError/TypeError base classes will be removed. Update any
except ValueError → except OdmlibValidationError and except TypeError → except OdmlibTypeError
before upgrading to v0.3.0.
The manual OIDRef classes in odmlib/odm_1_3_2/rules/oid_ref.py,
odmlib/define_2_0/rules/oid_ref.py, and odmlib/define_2_1/rules/oid_ref.py are deprecated
in v0.2.0 and will be removed in v0.3.0. Migrate to create_oid_checker():
# Before (deprecated):
from odmlib.odm_1_3_2.rules.oid_ref import OIDRef
checker = OIDRef()
# After:
from odmlib.oid_generator import create_oid_checker
checker = create_oid_checker("odm_1_3_2")0.1.4 - Previous Release
- Last release using legacy
setup.pypackaging - See git history for details