Repository: pepkit/peppy

Issue:
If 'annotation_sheet.csv' is without samples, throws 'NoneType' error
I decided to remove the samples in the annotation_sheet.csv to see what would happen. Error was thrown - `TypeError: 'NoneType' object is not iterable.` It would be nice to have it respond with `No samples found in your file` or something.

Comments:

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 22ea48b..3ecc382 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,16 @@
 
 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.35.6] -- 2023-06-27
+### Added
+- `orient` argument to `to_dict` method
+
+### Fixed
+- The name of the raw subsample object to match the actual name (list). Commit: #442
+
+### Changed
+- Reduced the number of items returned in the to_dict(extended=True) method to 3, with the name and description now stored in the config key.
+
 ## [0.35.5] -- 2023-03-27
 ### Fixed
 - A [bug](https://github.com/pepkit/peppy/issues/435) with custom sample ids
diff --git a/peppy/_version.py b/peppy/_version.py
index 322d9f8..a51f1e9 100644
--- a/peppy/_version.py
+++ b/peppy/_version.py
@@ -1,1 +1,1 @@
-__version__ = "0.35.5"
+__version__ = "0.35.6"
diff --git a/peppy/const.py b/peppy/const.py
index bf6cc1b..0f009a5 100644
--- a/peppy/const.py
+++ b/peppy/const.py
@@ -119,6 +119,6 @@ OTHER_CONSTANTS = [
 
 PEP_LATEST_VERSION = "2.1.0"
 SAMPLE_RAW_DICT_KEY = "_sample_dict"
-SUBSAMPLE_RAW_DICT_KEY = "_subsample_dict"
+SUBSAMPLE_RAW_LIST_KEY = "_subsample_list"
 
 __all__ = PROJECT_CONSTANTS + SAMPLE_CONSTANTS + OTHER_CONSTANTS
diff --git a/peppy/project.py b/peppy/project.py
index ee378e7..665d4b2 100644
--- a/peppy/project.py
+++ b/peppy/project.py
@@ -6,7 +6,7 @@ import os, sys
 from collections.abc import Mapping
 from contextlib import suppress
 from logging import getLogger
-from typing import Dict, Iterable, List, Tuple, Union
+from typing import Dict, Iterable, List, Tuple, Union, Literal, NoReturn
 
 import numpy as np
 import pandas as pd
@@ -57,7 +57,7 @@ from .const import (
     SAMPLE_TABLE_INDEX_KEY,
     SUBSAMPLE_DF_KEY,
     SUBSAMPLE_NAME_ATTR,
-    SUBSAMPLE_RAW_DICT_KEY,
+    SUBSAMPLE_RAW_LIST_KEY,
     SUBSAMPLE_TABLE_INDEX_KEY,
     SUBSAMPLE_TABLES_FILE_KEY,
 )
@@ -208,17 +208,19 @@ class Project(PathExAttMap):
         self[SAMPLE_DF_KEY] = pd.DataFrame(pep_dictionary[SAMPLE_RAW_DICT_KEY])
         self[CONFIG_KEY] = pep_dictionary[CONFIG_KEY]
 
-        if SUBSAMPLE_RAW_DICT_KEY in pep_dictionary:
-            if pep_dictionary[SUBSAMPLE_RAW_DICT_KEY]:
+        if SUBSAMPLE_RAW_LIST_KEY in pep_dictionary:
+            if pep_dictionary[SUBSAMPLE_RAW_LIST_KEY]:
                 self[SUBSAMPLE_DF_KEY] = [
                     pd.DataFrame(sub_a)
-                    for sub_a in pep_dictionary[SUBSAMPLE_RAW_DICT_KEY]
+                    for sub_a in pep_dictionary[SUBSAMPLE_RAW_LIST_KEY]
                 ]
-        if NAME_KEY in pep_dictionary:
-            self[NAME_KEY] = pep_dictionary[NAME_KEY]
+        if NAME_KEY in self[CONFIG_KEY]:
+            self[NAME_KEY] = self[CONFIG_KEY][NAME_KEY]
 
-        if DESC_KEY in pep_dictionary:
-            self[DESC_KEY] = pep_dictionary[DESC_KEY]
+        if DESC_KEY in self[CONFIG_KEY]:
+            self[DESC_KEY] = self[CONFIG_KEY][DESC_KEY]
+
+        self._set_indexes(self[CONFIG_KEY])
 
         self.create_samples(modify=False if self[SAMPLE_TABLE_FILE_KEY] else True)
         self._sample_table = self._get_table_from_samples(
@@ -227,25 +229,35 @@ class Project(PathExAttMap):
 
         return self
 
-    def to_dict(self, expand: bool = False, extended: bool = False) -> dict:
+    def to_dict(
+        self,
+        expand: bool = False,
+        extended: bool = False,
+        orient: Literal[
+            "dict", "list", "series", "split", "tight", "records", "index"
+        ] = "dict",
+    ) -> dict:
         """
         Convert the Project object to a dictionary.
 
         :param bool expand: whether to expand the paths
         :param bool extended: whether to produce complete project dict (used to reinit the project)
+        :param Literal orient: orientation of the returned df
         :return dict: a dictionary representation of the Project object
         """
         if extended:
             if self[SUBSAMPLE_DF_KEY] is not None:
-                sub_df = [sub_a.to_dict() for sub_a in self[SUBSAMPLE_DF_KEY]]
+                sub_df = [
+                    sub_a.to_dict(orient=orient) for sub_a in self[SUBSAMPLE_DF_KEY]
+                ]
             else:
                 sub_df = None
+            self[CONFIG_KEY][NAME_KEY] = self[NAME_KEY]
+            self[CONFIG_KEY][DESC_KEY] = self[DESC_KEY]
             p_dict = {
-                SAMPLE_RAW_DICT_KEY: self[SAMPLE_DF_KEY].to_dict(),
-                CONFIG_KEY: dict(self[CONFIG_KEY]),
-                SUBSAMPLE_RAW_DICT_KEY: sub_df,
-                NAME_KEY: self[NAME_KEY],
-                DESC_KEY: self[DESC_KEY],
+                SAMPLE_RAW_DICT_KEY: self[SAMPLE_DF_KEY].to_dict(orient=orient),
+                CONFIG_KEY: self[CONFIG_KEY].to_dict(expand=expand),
+                SUBSAMPLE_RAW_LIST_KEY: sub_df,
             }
         else:
             p_dict = self.config.to_dict(expand=expand)
@@ -258,6 +270,9 @@ class Project(PathExAttMap):
         Populate Project with Sample objects
         """
         self._samples: List[Sample] = self.load_samples()
+        if self.samples is None:
+            _LOGGER.info("No samples found in the project.")
+
         if modify:
             self.modify_samples()
         else:
@@ -326,14 +341,7 @@ class Project(PathExAttMap):
 
         _LOGGER.debug("Raw ({}) config data: {}".format(cfg_path, config))
 
-        self.st_index = (
-            config[SAMPLE_TABLE_INDEX_KEY] if SAMPLE_TABLE_INDEX_KEY in config else None
-        )
-        self.sst_index = (
-            config[SUBSAMPLE_TABLE_INDEX_KEY]
-            if SUBSAMPLE_TABLE_INDEX_KEY in config
-            else None
-        )
+        self._set_indexes(config)
         # recursively import configs
         if (
             PROJ_MODS_KEY in config
@@ -388,6 +396,23 @@ class Project(PathExAttMap):
         relative_vars = [CFG_SAMPLE_TABLE_KEY, CFG_SUBSAMPLE_TABLE_KEY]
         _make_sections_absolute(self[CONFIG_KEY], relative_vars, cfg_path)
 
+    def _set_indexes(self, config: Mapping) -> NoReturn:
+        """
+        Set sample and subsample indexes if they are different then Default
+
+        :param config: project config
+        """
+        self.st_index = (
+            config[SAMPLE_TABLE_INDEX_KEY]
+            if SAMPLE_TABLE_INDEX_KEY in config
+            else SAMPLE_NAME_ATTR
+        )
+        self.sst_index = (
+            config[SUBSAMPLE_TABLE_INDEX_KEY]
+            if SUBSAMPLE_TABLE_INDEX_KEY in config
+            else SUBSAMPLE_NAME_ATTR
+        )
+
     def load_samples(self):
         """
         Read the sample_table and subsample_tables into dataframes


Test patch:
diff --git a/tests/test_Project.py b/tests/test_Project.py
index b585e04..751c4ef 100644
--- a/tests/test_Project.py
+++ b/tests/test_Project.py
@@ -553,13 +553,18 @@ class TestPostInitSampleCreation:
         p2 = Project(cfg=example_pep_csv_path)
         assert not p1 == p2
 
-    @pytest.mark.parametrize("example_pep_cfg_path", ["append"], indirect=True)
-    def test_from_dict(self, example_pep_cfg_path):
+    @pytest.mark.parametrize(
+        "example_pep_cfg_path",
+        ["append", "custom_index", "imply", "subtables"],
+        indirect=True,
+    )
+    @pytest.mark.parametrize("orient", ["dict", "records"])
+    def test_from_dict(self, example_pep_cfg_path, orient):
         """
         Test initializing project from dict
         """
         p1 = Project(cfg=example_pep_cfg_path)
-        p1_dict = p1.to_dict(extended=True)
+        p1_dict = p1.to_dict(extended=True, orient=orient)
         del p1_dict["_config"]["sample_table"]
         p2 = Project().from_dict(p1_dict)
         assert p1 == p2