Skip to content

Commit a1bedea

Browse files
Merge pull request #489 from pepkit/minor_fixes
Release 0.40.3
2 parents fc15511 + 395f12f commit a1bedea

File tree

15 files changed

+146
-44
lines changed

15 files changed

+146
-44
lines changed

peppy/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.40.2"
1+
__version__ = "0.40.3"

peppy/const.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
SAMPLE_TABLE_INDEX_KEY = "sample_table_index"
1414
SUBSAMPLE_TABLE_INDEX_KEY = "subsample_table_index"
1515
CONFIG_KEY = "_config"
16+
ORIGINAL_CONFIG_KEY = "_original_config"
1617
PROJECT_TYPENAME = "Project"
1718
SAMPLE_MODS_KEY = "sample_modifiers"
1819
PROJ_MODS_KEY = "project_modifiers"
1920
NAME_TABLE_ATTR = "sample_table"
20-
CONSTANT_KEY = "append"
21+
APPEND_KEY = "append"
2122
REMOVE_KEY = "remove"
2223
DUPLICATED_KEY = "duplicate"
2324
DERIVED_KEY = "derive"
@@ -33,11 +34,11 @@
3334
AMENDMENTS_KEY = "amend"
3435
ACTIVE_AMENDMENTS_KEY = "_" + AMENDMENTS_KEY
3536
SAMPLE_EDIT_FLAG_KEY = "_samples_touched"
36-
SAMPLE_MODIFIERS = [CONSTANT_KEY, IMPLIED_KEY, DERIVED_KEY, DUPLICATED_KEY, REMOVE_KEY]
37+
SAMPLE_MODIFIERS = [APPEND_KEY, IMPLIED_KEY, DERIVED_KEY, DUPLICATED_KEY, REMOVE_KEY]
3738
PROJECT_MODIFIERS = [CFG_IMPORTS_KEY, AMENDMENTS_KEY]
3839
PROJECT_CONSTANTS = [
3940
"REQUIRED_VERSION",
40-
"CONSTANT_KEY",
41+
"APPEND_KEY",
4142
"DERIVED_SOURCES_KEY",
4243
"DERIVED_KEY",
4344
"SAMPLE_MODS_KEY",

peppy/project.py

+28-10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from rich.console import Console
1717
from rich.progress import track
1818
from ubiquerg import is_url
19+
from copy import deepcopy
1920

2021
from .const import (
2122
ACTIVE_AMENDMENTS_KEY,
@@ -27,7 +28,7 @@
2728
CONFIG_FILE_KEY,
2829
CONFIG_KEY,
2930
CONFIG_VERSION_KEY,
30-
CONSTANT_KEY,
31+
APPEND_KEY,
3132
DERIVED_ATTRS_KEY,
3233
DERIVED_KEY,
3334
DERIVED_SOURCES_KEY,
@@ -59,6 +60,7 @@
5960
SUBSAMPLE_RAW_LIST_KEY,
6061
SUBSAMPLE_TABLE_INDEX_KEY,
6162
SUBSAMPLE_TABLES_FILE_KEY,
63+
ORIGINAL_CONFIG_KEY,
6264
)
6365
from .exceptions import (
6466
InvalidSampleTableFileException,
@@ -137,7 +139,6 @@ def __init__(
137139

138140
self._samples = []
139141
self[SAMPLE_EDIT_FLAG_KEY] = False
140-
self.is_private = False
141142
self.progressbar = False
142143

143144
# table indexes can be specified in config or passed to the object constructor
@@ -200,6 +201,19 @@ def from_pandas(
200201
)
201202
return tmp_obj
202203

204+
@classmethod
205+
def from_pephub(cls, registry_path: str) -> "Project":
206+
"""
207+
Init project from pephubclient.
208+
209+
:param registry_path: PEPhub registry path
210+
:return: peppy Project
211+
"""
212+
from pephubclient import PEPHubClient
213+
214+
phc = PEPHubClient()
215+
return phc.load_project(project_registry_path=registry_path)
216+
203217
@classmethod
204218
def from_dict(cls, pep_dictionary: dict):
205219
"""
@@ -317,13 +331,13 @@ def to_dict(
317331
else:
318332
sub_df = None
319333
try:
320-
self[CONFIG_KEY][NAME_KEY] = self.name
334+
self[ORIGINAL_CONFIG_KEY][NAME_KEY] = self.name
321335
except NotImplementedError:
322-
self[CONFIG_KEY][NAME_KEY] = "unnamed"
323-
self[CONFIG_KEY][DESC_KEY] = self.description
336+
self[ORIGINAL_CONFIG_KEY][NAME_KEY] = "unnamed"
337+
self[ORIGINAL_CONFIG_KEY][DESC_KEY] = self.description
324338
p_dict = {
325339
SAMPLE_RAW_DICT_KEY: self[SAMPLE_DF_KEY].to_dict(orient=orient),
326-
CONFIG_KEY: dict(self[CONFIG_KEY]),
340+
CONFIG_KEY: dict(self[ORIGINAL_CONFIG_KEY]),
327341
SUBSAMPLE_RAW_LIST_KEY: sub_df,
328342
}
329343
else:
@@ -438,6 +452,7 @@ def parse_config_file(
438452
)
439453

440454
self[CONFIG_KEY].update(**config)
455+
self[ORIGINAL_CONFIG_KEY] = deepcopy(self[CONFIG_KEY])
441456
# Parse yaml into the project.config attributes
442457
_LOGGER.debug("Adding attributes: {}".format(", ".join(config)))
443458
# Overwrite any config entries with entries in the amendments
@@ -587,8 +602,8 @@ def attr_constants(self):
587602
Update each Sample with constants declared by a Project.
588603
If Project does not declare constants, no update occurs.
589604
"""
590-
if self._modifier_exists(CONSTANT_KEY):
591-
to_append = self[CONFIG_KEY][SAMPLE_MODS_KEY][CONSTANT_KEY]
605+
if self._modifier_exists(APPEND_KEY):
606+
to_append = self[CONFIG_KEY][SAMPLE_MODS_KEY][APPEND_KEY]
592607
_LOGGER.debug("Applying constant attributes: {}".format(to_append))
593608

594609
for s in track(
@@ -1309,14 +1324,17 @@ def _read_sample_data(self):
13091324
_LOGGER.info("No config key in Project, or reading project from dict")
13101325
return
13111326
if CFG_SAMPLE_TABLE_KEY not in self[CONFIG_KEY]:
1312-
_LOGGER.debug("no {} found".format(CFG_SAMPLE_TABLE_KEY))
1327+
_LOGGER.debug(f"No {CFG_SAMPLE_TABLE_KEY} found in config file")
13131328
return
13141329
st = self[CONFIG_KEY][CFG_SAMPLE_TABLE_KEY]
13151330

13161331
if self[SUBSAMPLE_TABLES_FILE_KEY] is not None:
13171332
sst = self[SUBSAMPLE_TABLES_FILE_KEY]
13181333
else:
1319-
if CONFIG_KEY in self and CFG_SUBSAMPLE_TABLE_KEY in self[CONFIG_KEY]:
1334+
if (
1335+
CONFIG_KEY in self
1336+
and self[CONFIG_KEY].get(CFG_SUBSAMPLE_TABLE_KEY) is not None
1337+
):
13201338
sst = make_list(self[CONFIG_KEY][CFG_SUBSAMPLE_TABLE_KEY], str)
13211339
else:
13221340
sst = None

peppy/sample.py

+27-18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from copy import copy as cp
55
from logging import getLogger
66
from string import Formatter
7+
from typing import Optional, Union
78

89
import pandas as pd
910
import yaml
@@ -134,31 +135,39 @@ def _obj2dict(obj, name=None):
134135
serial.update({"prj": grab_project_data(self[PRJ_REF])})
135136
return serial
136137

137-
def to_yaml(self, path, add_prj_ref=False):
138+
def to_yaml(
139+
self, path: Optional[str] = None, add_prj_ref=False
140+
) -> Union[str, None]:
138141
"""
139-
Serializes itself in YAML format.
142+
Serializes itself in YAML format. Writes to file if path is provided, else returns string representation.
140143
141144
:param str path: A file path to write yaml to; provide this or
142-
the subs_folder_path
145+
the subs_folder_path, defaults to None
143146
:param bool add_prj_ref: whether the project reference bound do the
144147
Sample object should be included in the YAML representation
148+
:return str | None: returns string representation of sample yaml or None
145149
"""
146150
serial = self.to_dict(add_prj_ref=add_prj_ref)
147-
path = os.path.expandvars(path)
148-
if not os.path.exists(os.path.dirname(path)):
149-
_LOGGER.warning(
150-
"Could not write sample data to: {}. "
151-
"Directory does not exist".format(path)
152-
)
153-
return
154-
with open(path, "w") as outfile:
155-
try:
156-
yaml_data = yaml.safe_dump(serial, default_flow_style=False)
157-
except yaml.representer.RepresenterError:
158-
_LOGGER.error("Serialized sample data: {}".format(serial))
159-
raise
160-
outfile.write(yaml_data)
161-
_LOGGER.debug("Sample data written to: {}".format(path))
151+
if path:
152+
path = os.path.expandvars(path)
153+
if os.path.exists(os.path.dirname(path)):
154+
with open(path, "w") as outfile:
155+
try:
156+
yaml_data = yaml.safe_dump(serial, default_flow_style=False)
157+
except yaml.representer.RepresenterError:
158+
_LOGGER.error("Serialized sample data: {}".format(serial))
159+
raise
160+
outfile.write(yaml_data)
161+
_LOGGER.debug("Sample data written to: {}".format(path))
162+
else:
163+
_LOGGER.warning(
164+
"Could not write sample data to: {}. "
165+
"Directory does not exist".format(path)
166+
)
167+
return
168+
else:
169+
yaml_data = yaml.safe_dump(serial, stream=None, default_flow_style=False)
170+
return yaml_data
162171

163172
def derive_attribute(self, data_sources, attr_name):
164173
"""

peppy/simple_attr_map.py

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def __getattr__(self, item):
4646
except KeyError:
4747
raise AttributeError(f"Attribute not found: {item}")
4848

49+
def __eq__(self, other: "SimpleAttMap"):
50+
return self._mapped_attr == other._mapped_attr
51+
4952
@property
5053
def attributes(self):
5154
return self._mapped_attr

peppy/utils.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
import os
5-
from typing import Dict, Mapping
5+
from typing import Dict, Mapping, Type, Union
66
from urllib.request import urlopen
77

88
import yaml
@@ -76,14 +76,16 @@ def grab_project_data(prj):
7676
raise KeyError("Project lacks section '{}'".format(CONFIG_KEY))
7777

7878

79-
def make_list(arg, obj_class):
79+
def make_list(arg: Union[list, str], obj_class: Type) -> list:
8080
"""
8181
Convert an object of predefined class to a list of objects of that class or
8282
ensure a list is a list of objects of that class
8383
8484
:param list[obj] | obj arg: string or a list of strings to listify
8585
:param str obj_class: name of the class of intrest
86+
8687
:return list: list of objects of the predefined class
88+
8789
:raise TypeError: if a faulty argument was provided
8890
"""
8991

requirements/requirements-all.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pyyaml
33
rich>=10.3.0
44
ubiquerg>=0.6.2
55
numpy
6+
pephubclient>=0.4.2

tests/conftest.py

-5
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ def example_pep_cfg_noname_path(request):
5151
return get_path_to_example_file(EPB, "noname", request.param)
5252

5353

54-
@pytest.fixture
55-
def example_pep_cfg_custom_index(request):
56-
return get_path_to_example_file(EPB, "custom_index", request.param)
57-
58-
5954
@pytest.fixture
6055
def example_peps_cfg_paths(request):
6156
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pep_version: "2.0.0"
2+
sample_table: "samplesheet.csv"
3+
subsample_table: "subsamplesheet.csv"
4+
sample_table_index: "sample"
5+
subsample_table_index: "sample"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
sample,strandedness
2+
WT_REP1,reverse
3+
WT_REP2,reverse
4+
RAP1_UNINDUCED_REP1,reverse
5+
RAP1_UNINDUCED_REP2,reverse
6+
RAP1_IAA_30M_REP1,reverse
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sample,instrument_platform,run_accession,fastq_1,fastq_2
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
3+
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
4+
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
5+
RAP1_UNINDUCED_REP1,COMPLETE_GENOMICS,somerunaccesion,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357073_1.fastq.gz,
6+
RAP1_UNINDUCED_REP2,DNBSEQ,ERR2412421,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357074_1.fastq.gz,
7+
RAP1_UNINDUCED_REP2,HELICOS,xxxxxxxxxx,https://raw.githubusercontent.com/nf-core/test-datasets/rnaseq/testdata/GSE110004/SRR6357075_1.fastq.gz,
8+
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pep_version: "2.0.0"
2+
sample_table: sample_table.csv
3+
subsample_table: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sample_name,protocol,file
2+
frog_1,anySampleType,multi
3+
frog_2,anySampleType,multi
4+
frog_3,anySampleType,multi

tests/smoketests/test_Sample.py

+31
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ def test_str_repr_correctness(self, example_pep_cfg_path):
5858
assert example_pep_cfg_path in str_repr
5959
assert "Sample '{}'".format(sample["sample_name"]) in str_repr
6060

61+
@pytest.mark.parametrize("example_pep_cfg_path", EXAMPLE_TYPES, indirect=True)
62+
def test_sample_to_yaml_no_path(self, example_pep_cfg_path):
63+
"""
64+
Verify that to_yaml returns representation without requiring a path.
65+
"""
66+
p = Project(cfg=example_pep_cfg_path)
67+
for sample in p.samples:
68+
yaml_repr = sample.to_yaml()
69+
assert "sample_name" in yaml_repr
70+
6171
@pytest.mark.parametrize("example_pep_cfg_path", ["basic"], indirect=True)
6272
def test_sheet_dict_excludes_private_attrs(self, example_pep_cfg_path):
6373
"""
@@ -77,3 +87,24 @@ def test_pickle_in_samples(self, example_pep_cfg_path):
7787
unpickled_sample = pickle.loads(pickled_data)
7888

7989
assert sample.to_dict() == unpickled_sample.to_dict()
90+
91+
@pytest.mark.parametrize("example_pep_cfg_path", ["basic"], indirect=True)
92+
def test_equals_samples(self, example_pep_cfg_path):
93+
p1 = Project(cfg=example_pep_cfg_path)
94+
p2 = Project(cfg=example_pep_cfg_path)
95+
s1 = p1.samples[0]
96+
s2 = p2.samples[0]
97+
98+
assert s1 == s2
99+
100+
@pytest.mark.parametrize("example_pep_cfg_path", ["basic"], indirect=True)
101+
def test_not_equals_samples(self, example_pep_cfg_path):
102+
p1 = Project(cfg=example_pep_cfg_path)
103+
p2 = Project(cfg=example_pep_cfg_path)
104+
s1 = p1.samples[0]
105+
s2 = p2.samples[0]
106+
s3 = p2.samples[1]
107+
108+
s2.new = "something"
109+
assert not s1 == s2
110+
assert not s1 == s3

tests/test_Project.py

+21-5
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,12 @@ def test_missing_sample_name_custom_index(self, example_pep_cfg_noname_path):
330330
p = Project(cfg=example_pep_cfg_noname_path, sample_table_index="id")
331331
assert p.sample_name_colname == "id"
332332

333-
@pytest.mark.parametrize(
334-
"example_pep_cfg_custom_index", ["project_config.yaml"], indirect=True
335-
)
336-
def test_sample_name_custom_index(self, example_pep_cfg_custom_index):
333+
@pytest.mark.parametrize("example_pep_cfg_path", ["custom_index"], indirect=True)
334+
def test_sample_name_custom_index(self, example_pep_cfg_path):
337335
"""
338336
Verify that sample_name attribute becomes st_index from cfg
339337
"""
340-
p = Project(cfg=example_pep_cfg_custom_index)
338+
p = Project(cfg=example_pep_cfg_path)
341339
assert p.sample_name_colname == "NOT_SAMPLE_NAME"
342340
assert p.samples[0].sample_name == "frog_1"
343341

@@ -726,3 +724,21 @@ def test_sample_len(self, example_pep_cfg_path):
726724
"""
727725
p = Project(cfg=example_pep_cfg_path)
728726
assert len(p.samples[0]) == 4
727+
728+
@pytest.mark.parametrize("example_pep_cfg_path", ["subsamples_none"], indirect=True)
729+
def test_config_with_subsample_null(self, example_pep_cfg_path):
730+
"""
731+
Tests if config can have value with subsample=null
732+
"""
733+
p = Project(cfg=example_pep_cfg_path)
734+
assert p.subsample_table is None
735+
736+
@pytest.mark.parametrize(
737+
"example_pep_cfg_path", ["nextflow_subsamples"], indirect=True
738+
)
739+
def test_nextflow_subsamples(self, example_pep_cfg_path):
740+
"""
741+
Tests if config can have value with subsample=null
742+
"""
743+
p = Project(cfg=example_pep_cfg_path)
744+
assert isinstance(p, Project)

0 commit comments

Comments
 (0)