Repository: scikit-hep/pyhf

Issue:
json2xml needs to add support for the HiFa DTD
# Description

json2xml does not include the DTD statement in the XML outputs that it generates.

# Expected Behavior

Should include DTD statement.

# Actual Behavior

Does not include DTD statement.

# Checklist

- [x] Run `git fetch` to get the most up to date version of `master`
- [x] Searched through existing Issues to confirm this is not a duplicate issue
- [x] Filled out the Description, Expected Behavior, Actual Behavior, and Steps to Reproduce sections above or have edited/removed them in a way that fully describes the issue



Discussion:

Resolving patch:
diff --git a/pyhf/writexml.py b/pyhf/writexml.py
--- a/pyhf/writexml.py
+++ b/pyhf/writexml.py
@@ -1,6 +1,8 @@
 import logging
 
 import os
+import shutil
+import pkg_resources
 import xml.etree.cElementTree as ET
 import numpy as np
 import uproot
@@ -68,7 +70,13 @@ def build_measurement(measurementspec):
             lumierr = parameter['sigmas'][0]
 
     # define measurement
-    meas = ET.Element("Measurement", Name=name, Lumi=str(lumi), LumiRelErr=str(lumierr))
+    meas = ET.Element(
+        "Measurement",
+        Name=name,
+        Lumi=str(lumi),
+        LumiRelErr=str(lumierr),
+        ExportOnly=str(True),
+    )
     poiel = ET.Element('POI')
     poiel.text = poi
     meas.append(poiel)
@@ -115,6 +123,7 @@ def build_modifier(modifierspec, channelname, samplename, sampledata):
         attrs['HistoName'] = _make_hist_name(
             channelname, samplename, modifierspec['name']
         )
+        del attrs['Name']
         # need to make this a relative uncertainty stored in ROOT file
         _export_root_histogram(
             attrs['HistoName'], np.divide(modifierspec['data'], sampledata).tolist()
@@ -184,6 +193,10 @@ def build_channel(channelspec, dataspec):
 def writexml(spec, specdir, data_rootdir, resultprefix):
     global _ROOT_DATA_FILE
 
+    shutil.copyfile(
+        pkg_resources.resource_filename(__name__, 'data/HistFactorySchema.dtd'),
+        os.path.join(os.path.dirname(specdir), 'HistFactorySchema.dtd'),
+    )
     combination = ET.Element(
         "Combination", OutputFilePrefix=os.path.join('.', specdir, resultprefix)
     )
@@ -196,6 +209,9 @@ def writexml(spec, specdir, data_rootdir, resultprefix):
             with open(channelfilename, 'w') as channelfile:
                 channel = build_channel(channelspec, spec.get('data'))
                 indent(channel)
+                channelfile.write(
+                    "<!DOCTYPE Channel SYSTEM '../HistFactorySchema.dtd'>\n\n"
+                )
                 channelfile.write(
                     ET.tostring(channel, encoding='utf-8').decode('utf-8')
                 )
@@ -207,4 +223,6 @@ def writexml(spec, specdir, data_rootdir, resultprefix):
     for measurement in spec['toplvl']['measurements']:
         combination.append(build_measurement(measurement))
     indent(combination)
-    return ET.tostring(combination, encoding='utf-8')
+    return "<!DOCTYPE Combination  SYSTEM 'HistFactorySchema.dtd'>\n\n".encode(
+        "utf-8"
+    ) + ET.tostring(combination, encoding='utf-8')


Test patch:
diff --git a/tests/test_export.py b/tests/test_export.py
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -197,7 +197,9 @@ def test_export_modifier(mocker, spec, has_root_data, attrs):
     modifier = pyhf.writexml.build_modifier(
         modifierspec, channelname, samplename, sampledata
     )
-    assert modifier.attrib['Name'] == modifierspec['name']
+    # if the modifier is a staterror, it has no Name
+    if 'Name' in modifier.attrib:
+        assert modifier.attrib['Name'] == modifierspec['name']
     assert all(attr in modifier.attrib for attr in attrs)
     assert pyhf.writexml._ROOT_DATA_FILE.__setitem__.called == has_root_data