Repository: bihealth/altamisa

Issue:
Add list processing for Performer fields
We allow semicolon-separated lists in `Parameter Value` fields, but not in special parameter `Performer` fields. As many experiments are a team effort, it makes total sense to support referencing more than one name here.

- add to model
- adapt parser
- adapt validator
- add tests

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bihealth/altamisa/pull/141?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`58aa5d0`)](https://app.codecov.io/gh/bihealth/altamisa/commit/58aa5d034d836dd5ae75fd73bac2f4f8f149bfd7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth) 96.52% compared to head [(`b15af8e`)](https://app.codecov.io/gh/bihealth/altamisa/pull/141?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth) 96.52%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master     #141   +/-   ##
=======================================
  Coverage   96.52%   96.52%           
=======================================
  Files          29       29           
  Lines        3426     3426           
=======================================
  Hits         3307     3307           
  Misses        119      119           
```

| [Flag](https://app.codecov.io/gh/bihealth/altamisa/pull/141/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth) | Coverage Δ | |
|---|---|---|
| [python](https://app.codecov.io/gh/bihealth/altamisa/pull/141/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth) | `96.52% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bihealth/altamisa/pull/141?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bihealth).


Patch:
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 8265e77..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,1 +0,0 @@
-altamisa/_version.py export-subst
diff --git a/altamisa/__init__.py b/altamisa/__init__.py
index ad5cc75..5ba63ea 100644
--- a/altamisa/__init__.py
+++ b/altamisa/__init__.py
@@ -1,1 +1,1 @@
-from ._version import __version__  # noqa
+from .version import __version__  # noqa
diff --git a/altamisa/isatab/models.py b/altamisa/isatab/models.py
index 2481d60..531da90 100644
--- a/altamisa/isatab/models.py
+++ b/altamisa/isatab/models.py
@@ -460,7 +460,7 @@ class Process:
     #: Process date
     date: Optional[Union[datetime.date, Literal[""]]]
     #: Performer of process
-    performer: Optional[str]
+    performer: Optional[Tuple[str, ...]]
     #: Tuple of parameters values
     parameter_values: Tuple[ParameterValue, ...]
     #: Tuple of process comments
diff --git a/altamisa/isatab/parse_assay_study.py b/altamisa/isatab/parse_assay_study.py
index b75ec0b..142efa0 100644
--- a/altamisa/isatab/parse_assay_study.py
+++ b/altamisa/isatab/parse_assay_study.py
@@ -442,7 +442,7 @@ class _ProcessBuilder(_NodeBuilderBase[models.Process]):
         else:
             date = None
         if self.performer_header:
-            performer = line[self.performer_header.col_no]
+            performer = tuple(self._token_with_escape(line[self.performer_header.col_no]))
         else:
             performer = None
         comments = tuple(
diff --git a/altamisa/isatab/write_assay_study.py b/altamisa/isatab/write_assay_study.py
index d711d5d..eb0f8ce 100644
--- a/altamisa/isatab/write_assay_study.py
+++ b/altamisa/isatab/write_assay_study.py
@@ -411,7 +411,9 @@ class _WriterBase:
         if node.protocol_ref != TOKEN_UNKNOWN:
             attributes[table_headers.PROTOCOL_REF] = node.protocol_ref
         if node.performer is not None:
-            attributes[table_headers.PERFORMER] = node.performer
+            attributes[table_headers.PERFORMER] = ";".join(
+                [p.replace(";", "\\;") if p else "" for p in node.performer]
+            )
         if node.date is not None:
             attributes[table_headers.DATE] = node.date
         for parameter in node.parameter_values:
diff --git a/altamisa/_version.py b/altamisa/version.py
similarity index 100%
rename from altamisa/_version.py
rename to altamisa/version.py
diff --git a/docs/index.rst b/docs/index.rst
index 9e2d7a3..baae2e3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -53,10 +53,10 @@ Special Extensions
 In addition to the original ISA-Tab format specifications, AltamISA supports
  the following special modifications to improve specific use cases:
 
-- **List of values** in ``Characterics``, ``Parameter Value``, or ``Factor Value`` fields by using
-  semicolon-separators (";"). Note, for ontology terms the same number of
-  splits is expected in the associated field ``Term Source REF`` and
-  ``Term Accession Number``.
+- **List of values** in ``Characterics``, ``Parameter Value``, ``Factor Value``, 
+  and ``Performer`` fields by using semicolon-separators (";"). Note: For 
+  ontology terms the same number of splits is expected in the associated field 
+  ``Term Source REF`` and ``Term Accession Number``.
 - **Material name** ``Library Name`` for improved library
   annotation in nucleotide sequencing assays.
 
diff --git a/setup.cfg b/setup.cfg
index 116d00a..382a18a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -21,5 +21,5 @@ testpaths = tests
 
 [coverage:run]
 omit =
-    altamisa/_version.py
+    altamisa/version.py
 relative_files = True
diff --git a/setup.py b/setup.py
index 26e2d5b..947df3b 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@ install_requirements = parse_requirements("requirements/base.txt")
 
 package_root = os.path.abspath(os.path.dirname(__file__))
 version = {}
-with open(os.path.join(package_root, "altamisa/_version.py")) as fp:
+with open(os.path.join(package_root, "altamisa/version.py")) as fp:
     exec(fp.read(), version)
 version = version["__version__"]
 


Test patch:
diff --git a/tests/test_parse_study.py b/tests/test_parse_study.py
index ffaaa64..b2331ac 100644
--- a/tests/test_parse_study.py
+++ b/tests/test_parse_study.py
@@ -257,7 +257,7 @@ def test_study_row_reader_small_study(small_investigation_file, small_study_file
         None,
         None,
         date(2018, 2, 2),
-        "John Doe",
+        ("John Doe",),
         (models.ParameterValue("instrument", ["scalpel"], None),),
         (),
         None,
@@ -298,7 +298,7 @@ def test_study_row_reader_small_study(small_investigation_file, small_study_file
         None,
         None,
         date(2018, 2, 2),
-        "John Doe",
+        ("John Doe",),
         (models.ParameterValue("instrument", ["scalpel type A", "scalpel type B"], None),),
         (),
         None,
@@ -507,7 +507,7 @@ def test_study_reader_small_study(
         None,
         None,
         date(2018, 2, 2),
-        "John Doe",
+        ("John Doe",),
         (models.ParameterValue("instrument", ["scalpel"], None),),
         (),
         None,
@@ -522,7 +522,7 @@ def test_study_reader_small_study(
         None,
         None,
         date(2018, 2, 2),
-        "John Doe",
+        ("John Doe",),
         (models.ParameterValue("instrument", ["scalpel type A", "scalpel type B"], None),),
         (),
         None,
@@ -537,7 +537,7 @@ def test_study_reader_small_study(
         None,
         None,
         date(2018, 2, 2),
-        "John Doe",
+        ("John Doe",),
         (models.ParameterValue("instrument", ["scalpel"], None),),
         (),
         None,