Repository: pepkit/peppy

Issue:
String yaml representation of sample
Is there any way for me to get a straight-up yaml representation of a sample? I found `sample.to_yaml()` but this appears to only be able to output to a file.

I can also just `print(sample)`, but then I get all this extra stuff too:

```
Sample 'sample1' in Project (/home/nsheff/code/refgenie_looper_demo/pep_bio.yaml)

sample_name:         sample1
protocol:            RNA-seq
organism:            human
read1:               data/sample1_1.fq.gz
read2:               data/sample1_2.fq.gz
Index:               /home/nsheff/code/refgenie_sandbox/alias/t7/bwa_index/default/t7.fa
pipeline_interfaces: pipeline_interface.yaml
InputFile1:          data/sample1_1.fq.gz
InputFile2:          data/sample1_2.fq.gz
genome:              t7
prealignments:       t7, t7, t7

...                (showing first 10)
```

What I need is just a function that gives me the yaml representation of the sample.

Discussion:

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 4a96c87..4db79aa 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,10 @@
 
 This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format.
 
+## [0.40.2] -- 2024-05-28
+### Added
+- added `sample_name` property to samples object.
+
 ## [0.40.1] -- 2024-01-11
 ### Fixed
 - Initializing Project with `NaN` value instead of `None` in `from_pandas` method
diff --git a/peppy/_version.py b/peppy/_version.py
index 01b570a..47a53c1 100644
--- a/peppy/_version.py
+++ b/peppy/_version.py
@@ -1,1 +1,1 @@
-__version__ = "0.40.1"
+__version__ = "0.40.2"
diff --git a/peppy/const.py b/peppy/const.py
index b52c6cd..0f009a5 100644
--- a/peppy/const.py
+++ b/peppy/const.py
@@ -13,12 +13,11 @@ SUBSAMPLE_TABLES_FILE_KEY = "_subsample_tables_path"
 SAMPLE_TABLE_INDEX_KEY = "sample_table_index"
 SUBSAMPLE_TABLE_INDEX_KEY = "subsample_table_index"
 CONFIG_KEY = "_config"
-ORIGINAL_CONFIG_KEY = "_original_config"
 PROJECT_TYPENAME = "Project"
 SAMPLE_MODS_KEY = "sample_modifiers"
 PROJ_MODS_KEY = "project_modifiers"
 NAME_TABLE_ATTR = "sample_table"
-APPEND_KEY = "append"
+CONSTANT_KEY = "append"
 REMOVE_KEY = "remove"
 DUPLICATED_KEY = "duplicate"
 DERIVED_KEY = "derive"
@@ -34,11 +33,11 @@ CFG_IMPORTS_KEY = "import"
 AMENDMENTS_KEY = "amend"
 ACTIVE_AMENDMENTS_KEY = "_" + AMENDMENTS_KEY
 SAMPLE_EDIT_FLAG_KEY = "_samples_touched"
-SAMPLE_MODIFIERS = [APPEND_KEY, IMPLIED_KEY, DERIVED_KEY, DUPLICATED_KEY, REMOVE_KEY]
+SAMPLE_MODIFIERS = [CONSTANT_KEY, IMPLIED_KEY, DERIVED_KEY, DUPLICATED_KEY, REMOVE_KEY]
 PROJECT_MODIFIERS = [CFG_IMPORTS_KEY, AMENDMENTS_KEY]
 PROJECT_CONSTANTS = [
     "REQUIRED_VERSION",
-    "APPEND_KEY",
+    "CONSTANT_KEY",
     "DERIVED_SOURCES_KEY",
     "DERIVED_KEY",
     "SAMPLE_MODS_KEY",
diff --git a/peppy/project.py b/peppy/project.py
index d87fd6c..8b56260 100644
--- a/peppy/project.py
+++ b/peppy/project.py
@@ -16,7 +16,6 @@ from pandas.core.common import flatten
 from rich.console import Console
 from rich.progress import track
 from ubiquerg import is_url
-from copy import deepcopy
 
 from .const import (
     ACTIVE_AMENDMENTS_KEY,
@@ -28,7 +27,7 @@ from .const import (
     CONFIG_FILE_KEY,
     CONFIG_KEY,
     CONFIG_VERSION_KEY,
-    APPEND_KEY,
+    CONSTANT_KEY,
     DERIVED_ATTRS_KEY,
     DERIVED_KEY,
     DERIVED_SOURCES_KEY,
@@ -60,7 +59,6 @@ from .const import (
     SUBSAMPLE_RAW_LIST_KEY,
     SUBSAMPLE_TABLE_INDEX_KEY,
     SUBSAMPLE_TABLES_FILE_KEY,
-    ORIGINAL_CONFIG_KEY,
 )
 from .exceptions import (
     InvalidSampleTableFileException,
@@ -139,6 +137,7 @@ class Project(MutableMapping):
 
         self._samples = []
         self[SAMPLE_EDIT_FLAG_KEY] = False
+        self.is_private = False
         self.progressbar = False
 
         # table indexes can be specified in config or passed to the object constructor
@@ -318,13 +317,13 @@ class Project(MutableMapping):
             else:
                 sub_df = None
             try:
-                self[ORIGINAL_CONFIG_KEY][NAME_KEY] = self.name
+                self[CONFIG_KEY][NAME_KEY] = self.name
             except NotImplementedError:
-                self[ORIGINAL_CONFIG_KEY][NAME_KEY] = "unnamed"
-            self[ORIGINAL_CONFIG_KEY][DESC_KEY] = self.description
+                self[CONFIG_KEY][NAME_KEY] = "unnamed"
+            self[CONFIG_KEY][DESC_KEY] = self.description
             p_dict = {
                 SAMPLE_RAW_DICT_KEY: self[SAMPLE_DF_KEY].to_dict(orient=orient),
-                CONFIG_KEY: dict(self[ORIGINAL_CONFIG_KEY]),
+                CONFIG_KEY: dict(self[CONFIG_KEY]),
                 SUBSAMPLE_RAW_LIST_KEY: sub_df,
             }
         else:
@@ -439,7 +438,6 @@ class Project(MutableMapping):
                     )
 
         self[CONFIG_KEY].update(**config)
-        self[ORIGINAL_CONFIG_KEY] = deepcopy(self[CONFIG_KEY])
         # Parse yaml into the project.config attributes
         _LOGGER.debug("Adding attributes: {}".format(", ".join(config)))
         # Overwrite any config entries with entries in the amendments
@@ -589,8 +587,8 @@ class Project(MutableMapping):
         Update each Sample with constants declared by a Project.
         If Project does not declare constants, no update occurs.
         """
-        if self._modifier_exists(APPEND_KEY):
-            to_append = self[CONFIG_KEY][SAMPLE_MODS_KEY][APPEND_KEY]
+        if self._modifier_exists(CONSTANT_KEY):
+            to_append = self[CONFIG_KEY][SAMPLE_MODS_KEY][CONSTANT_KEY]
             _LOGGER.debug("Applying constant attributes: {}".format(to_append))
 
             for s in track(
@@ -1311,17 +1309,14 @@ class Project(MutableMapping):
                 _LOGGER.info("No config key in Project, or reading project from dict")
                 return
             if CFG_SAMPLE_TABLE_KEY not in self[CONFIG_KEY]:
-                _LOGGER.debug(f"No {CFG_SAMPLE_TABLE_KEY} found in config file")
+                _LOGGER.debug("no {} found".format(CFG_SAMPLE_TABLE_KEY))
                 return
             st = self[CONFIG_KEY][CFG_SAMPLE_TABLE_KEY]
 
         if self[SUBSAMPLE_TABLES_FILE_KEY] is not None:
             sst = self[SUBSAMPLE_TABLES_FILE_KEY]
         else:
-            if (
-                CONFIG_KEY in self
-                and self[CONFIG_KEY].get(CFG_SUBSAMPLE_TABLE_KEY) is not None
-            ):
+            if CONFIG_KEY in self and CFG_SUBSAMPLE_TABLE_KEY in self[CONFIG_KEY]:
                 sst = make_list(self[CONFIG_KEY][CFG_SUBSAMPLE_TABLE_KEY], str)
             else:
                 sst = None
diff --git a/peppy/sample.py b/peppy/sample.py
index 9f92a6a..60d1467 100644
--- a/peppy/sample.py
+++ b/peppy/sample.py
@@ -4,6 +4,7 @@ from collections.abc import Mapping
 from copy import copy as cp
 from logging import getLogger
 from string import Formatter
+from typing import Optional, Union
 
 import pandas as pd
 import yaml
@@ -134,31 +135,39 @@ class Sample(SimpleAttMap):
             serial.update({"prj": grab_project_data(self[PRJ_REF])})
         return serial
 
-    def to_yaml(self, path, add_prj_ref=False):
+    def to_yaml(
+        self, path: Optional[str] = None, add_prj_ref=False
+    ) -> Union[str, None]:
         """
-        Serializes itself in YAML format.
+        Serializes itself in YAML format. Writes to file if path is provided, else returns string representation.
 
         :param str path: A file path to write yaml to; provide this or
-            the subs_folder_path
+            the subs_folder_path, defaults to None
         :param bool add_prj_ref: whether the project reference bound do the
             Sample object should be included in the YAML representation
+        :return str | None: returns string representation of sample yaml or None
         """
         serial = self.to_dict(add_prj_ref=add_prj_ref)
-        path = os.path.expandvars(path)
-        if not os.path.exists(os.path.dirname(path)):
-            _LOGGER.warning(
-                "Could not write sample data to: {}. "
-                "Directory does not exist".format(path)
-            )
-            return
-        with open(path, "w") as outfile:
-            try:
-                yaml_data = yaml.safe_dump(serial, default_flow_style=False)
-            except yaml.representer.RepresenterError:
-                _LOGGER.error("Serialized sample data: {}".format(serial))
-                raise
-            outfile.write(yaml_data)
-            _LOGGER.debug("Sample data written to: {}".format(path))
+        if path:
+            path = os.path.expandvars(path)
+            if os.path.exists(os.path.dirname(path)):
+                with open(path, "w") as outfile:
+                    try:
+                        yaml_data = yaml.safe_dump(serial, default_flow_style=False)
+                    except yaml.representer.RepresenterError:
+                        _LOGGER.error("Serialized sample data: {}".format(serial))
+                        raise
+                    outfile.write(yaml_data)
+                    _LOGGER.debug("Sample data written to: {}".format(path))
+            else:
+                _LOGGER.warning(
+                    "Could not write sample data to: {}. "
+                    "Directory does not exist".format(path)
+                )
+                return
+        else:
+            yaml_data = yaml.safe_dump(serial, stream=None, default_flow_style=False)
+            return yaml_data
 
     def derive_attribute(self, data_sources, attr_name):
         """
@@ -302,6 +311,16 @@ class Sample(SimpleAttMap):
         """
         return self[PRJ_REF]
 
+    @property
+    def sample_name(self):
+        """
+        Get the sample's name
+
+        :return str: current sample name derived from project's st_index
+        """
+
+        return self[self[PRJ_REF].st_index]
+
     # The __reduce__ function provides an interface for
     # correct object serialization with the pickle module.
     def __reduce__(self):
diff --git a/peppy/simple_attr_map.py b/peppy/simple_attr_map.py
index 6ce0322..50b1a08 100644
--- a/peppy/simple_attr_map.py
+++ b/peppy/simple_attr_map.py
@@ -46,9 +46,6 @@ class SimpleAttMap(MutableMapping):
         except KeyError:
             raise AttributeError(f"Attribute not found: {item}")
 
-    def __eq__(self, other: "SimpleAttMap"):
-        return self._mapped_attr == other._mapped_attr
-
     @property
     def attributes(self):
         return self._mapped_attr
diff --git a/peppy/utils.py b/peppy/utils.py
index d7598e7..35e6017 100644
--- a/peppy/utils.py
+++ b/peppy/utils.py
@@ -2,7 +2,7 @@
 
 import logging
 import os
-from typing import Dict, Mapping, Type, Union
+from typing import Dict, Mapping
 from urllib.request import urlopen
 
 import yaml
@@ -76,16 +76,14 @@ def grab_project_data(prj):
         raise KeyError("Project lacks section '{}'".format(CONFIG_KEY))
 
 
-def make_list(arg: Union[list, str], obj_class: Type) -> list:
+def make_list(arg, obj_class):
     """
     Convert an object of predefined class to a list of objects of that class or
     ensure a list is a list of objects of that class
 
     :param list[obj] | obj arg: string or a list of strings to listify
     :param str obj_class: name of the class of intrest
-
     :return list: list of objects of the predefined class
-
     :raise TypeError: if a faulty argument was provided
     """
 


Test patch:
diff --git a/tests/data/example_peps-master/example_custom_index/project_config.yaml b/tests/data/example_peps-master/example_custom_index/project_config.yaml
index 83f85a8..a9a777b 100644
--- a/tests/data/example_peps-master/example_custom_index/project_config.yaml
+++ b/tests/data/example_peps-master/example_custom_index/project_config.yaml
@@ -1,3 +1,3 @@
 pep_version: "2.0.0"
 sample_table: sample_table.csv
-sample_table_index: sample_id
+sample_table_index: NOT_SAMPLE_NAME
diff --git a/tests/data/example_peps-master/example_custom_index/sample_table.csv b/tests/data/example_peps-master/example_custom_index/sample_table.csv
index 3022814..c996542 100644
--- a/tests/data/example_peps-master/example_custom_index/sample_table.csv
+++ b/tests/data/example_peps-master/example_custom_index/sample_table.csv
@@ -1,3 +1,3 @@
-sample_id,protocol,file
+NOT_SAMPLE_NAME,protocol,file
 frog_1,anySampleType,data/frog1_data.txt
 frog_2,anySampleType,data/frog2_data.txt
diff --git a/tests/data/example_peps-master/example_nextflow_subsamples/project_config.yaml b/tests/data/example_peps-master/example_nextflow_subsamples/project_config.yaml
deleted file mode 100644
index ae6035f..0000000
--- a/tests/data/example_peps-master/example_nextflow_subsamples/project_config.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-pep_version: "2.0.0"
-sample_table: "samplesheet.csv"
-subsample_table: "subsamplesheet.csv"
-sample_table_index: "sample"
-subsample_table_index: "sample"
diff --git a/tests/data/example_peps-master/example_nextflow_subsamples/samplesheet.csv b/tests/data/example_peps-master/example_nextflow_subsamples/samplesheet.csv
deleted file mode 100644
index a26933e..0000000
--- a/tests/data/example_peps-master/example_nextflow_subsamples/samplesheet.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-sample,strandedness
-WT_REP1,reverse
-WT_REP2,reverse
-RAP1_UNINDUCED_REP1,reverse
-RAP1_UNINDUCED_REP2,reverse
-RAP1_IAA_30M_REP1,reverse
diff --git a/tests/data/example_peps-master/example_nextflow_subsamples/subsamplesheet.csv b/tests/data/example_peps-master/example_nextflow_subsamples/subsamplesheet.csv
deleted file mode 100644
index 1e56c36..0000000
--- a/tests/data/example_peps-master/example_nextflow_subsamples/subsamplesheet.csv
+++ /dev/null
@@ -1,8 +0,0 @@
-sample,instrument_platform,run_accession,fastq_1,fastq_2
-WT_REP1,ABI_SOLID,runaccession1,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357070_1.fastq.gz,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357070_2.fastq.gz
-WT_REP1,BGISEQ,runaccession2,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357071_1.fastq.gz,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357071_2.fastq.gz
-WT_REP2,CAPILLARY,123123123,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357072_1.fastq.gz,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357072_2.fastq.gz
-RAP1_UNINDUCED_REP1,COMPLETE_GENOMICS,somerunaccesion,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357073_1.fastq.gz,
-RAP1_UNINDUCED_REP2,DNBSEQ,ERR2412421,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357074_1.fastq.gz,
-RAP1_UNINDUCED_REP2,HELICOS,xxxxxxxxxx,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357075_1.fastq.gz,
-RAP1_IAA_30M_REP1,ILLUMINA,None,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357076_1.fastq.gz,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357076_2.fastq.gz
diff --git a/tests/data/example_peps-master/example_subsamples_none/project_config.yaml b/tests/data/example_peps-master/example_subsamples_none/project_config.yaml
deleted file mode 100644
index d90f126..0000000
--- a/tests/data/example_peps-master/example_subsamples_none/project_config.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-pep_version: "2.0.0"
-sample_table: sample_table.csv
-subsample_table: null
diff --git a/tests/data/example_peps-master/example_subsamples_none/sample_table.csv b/tests/data/example_peps-master/example_subsamples_none/sample_table.csv
deleted file mode 100644
index a800e04..0000000
--- a/tests/data/example_peps-master/example_subsamples_none/sample_table.csv
+++ /dev/null
@@ -1,4 +0,0 @@
-sample_name,protocol,file
-frog_1,anySampleType,multi
-frog_2,anySampleType,multi
-frog_3,anySampleType,multi
diff --git a/tests/smoketests/test_Sample.py b/tests/smoketests/test_Sample.py
index 2815c3c..77e42fb 100644
--- a/tests/smoketests/test_Sample.py
+++ b/tests/smoketests/test_Sample.py
@@ -58,6 +58,16 @@ class TestSample:
             assert example_pep_cfg_path in str_repr
             assert "Sample '{}'".format(sample["sample_name"]) in str_repr
 
+    @pytest.mark.parametrize("example_pep_cfg_path", EXAMPLE_TYPES, indirect=True)
+    def test_sample_to_yaml_no_path(self, example_pep_cfg_path):
+        """
+        Verify that to_yaml returns representation without requiring a path.
+        """
+        p = Project(cfg=example_pep_cfg_path)
+        for sample in p.samples:
+            yaml_repr = sample.to_yaml()
+            assert "sample_name" in yaml_repr
+
     @pytest.mark.parametrize("example_pep_cfg_path", ["basic"], indirect=True)
     def test_sheet_dict_excludes_private_attrs(self, example_pep_cfg_path):
         """
@@ -77,24 +87,3 @@ class TestSample:
             unpickled_sample = pickle.loads(pickled_data)
 
             assert sample.to_dict() == unpickled_sample.to_dict()
-
-    @pytest.mark.parametrize("example_pep_cfg_path", ["basic"], indirect=True)
-    def test_equals_samples(self, example_pep_cfg_path):
-        p1 = Project(cfg=example_pep_cfg_path)
-        p2 = Project(cfg=example_pep_cfg_path)
-        s1 = p1.samples[0]
-        s2 = p2.samples[0]
-
-        assert s1 == s2
-
-    @pytest.mark.parametrize("example_pep_cfg_path", ["basic"], indirect=True)
-    def test_not_equals_samples(self, example_pep_cfg_path):
-        p1 = Project(cfg=example_pep_cfg_path)
-        p2 = Project(cfg=example_pep_cfg_path)
-        s1 = p1.samples[0]
-        s2 = p2.samples[0]
-        s3 = p2.samples[1]
-
-        s2.new = "something"
-        assert not s1 == s2
-        assert not s1 == s3
diff --git a/tests/test_Project.py b/tests/test_Project.py
index ed2b07c..959ae08 100644
--- a/tests/test_Project.py
+++ b/tests/test_Project.py
@@ -330,6 +330,15 @@ class TestProjectConstructor:
         p = Project(cfg=example_pep_cfg_noname_path, sample_table_index="id")
         assert p.sample_name_colname == "id"
 
+    @pytest.mark.parametrize("example_pep_cfg_path", ["custom_index"], indirect=True)
+    def test_sample_name_custom_index(self, example_pep_cfg_path):
+        """
+        Verify that sample_name attribute becomes st_index from cfg
+        """
+        p = Project(cfg=example_pep_cfg_path)
+        assert p.sample_name_colname == "NOT_SAMPLE_NAME"
+        assert p.samples[0].sample_name == "frog_1"
+
     @pytest.mark.parametrize(
         "example_pep_cfg_path",
         ["basic"],
@@ -715,21 +724,3 @@ class TestSampleAttrMap:
         """
         p = Project(cfg=example_pep_cfg_path)
         assert len(p.samples[0]) == 4
-
-    @pytest.mark.parametrize("example_pep_cfg_path", ["subsamples_none"], indirect=True)
-    def test_config_with_subsample_null(self, example_pep_cfg_path):
-        """
-        Tests if config can have value with subsample=null
-        """
-        p = Project(cfg=example_pep_cfg_path)
-        assert p.subsample_table is None
-
-    @pytest.mark.parametrize(
-        "example_pep_cfg_path", ["nextflow_subsamples"], indirect=True
-    )
-    def test_nextflow_subsamples(self, example_pep_cfg_path):
-        """
-        Tests if config can have value with subsample=null
-        """
-        p = Project(cfg=example_pep_cfg_path)
-        assert isinstance(p, Project)