• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PyMassSpec / mh_utils / 22200682966

19 Feb 2026 09:23PM UTC coverage: 88.773% (-0.06%) from 88.836%
22200682966

push

github

domdfcoding
Add missing type hints

70 of 71 new or added lines in 10 files covered. (98.59%)

4 existing lines in 2 files now uncovered.

933 of 1051 relevant lines covered (88.77%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.95
/mh_utils/csv_parser/classes.py
1
#!/usr/bin/env python3
2
#
3
#  classes.py
4
"""
5
Classes to model parts of MassHunter CSV files.
6

7
.. versionadded:: 0.2.0
8
"""
9
#
10
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
11
#
12
#  Permission is hereby granted, free of charge, to any person obtaining a copy
13
#  of this software and associated documentation files (the "Software"), to deal
14
#  in the Software without restriction, including without limitation the rights
15
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
#  copies of the Software, and to permit persons to whom the Software is
17
#  furnished to do so, subject to the following conditions:
18
#
19
#  The above copyright notice and this permission notice shall be included in all
20
#  copies or substantial portions of the Software.
21
#
22
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
25
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
26
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
27
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
28
#  OR OTHER DEALINGS IN THE SOFTWARE.
29
#
30

31
# stdlib
32
from collections import OrderedDict
1✔
33
from decimal import Decimal
1✔
34
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Type, TypeVar, Union
1✔
35

36
# 3rd party
37
import numpy
1✔
38
import pandas
1✔
39
import sdjson
1✔
40
from cawdrey import AlphaDict
1✔
41
from domdf_python_tools import doctools
1✔
42
from domdf_python_tools.doctools import prettify_docstrings
1✔
43
from domdf_python_tools.paths import PathPlus
1✔
44
from domdf_python_tools.typing import PathLike
1✔
45

46
# this package
47
from mh_utils import Dictable
1✔
48

49
__all__ = [
1✔
50
                "Sample",
51
                "Result",
52
                "SampleList",
53
                "BaseSamplePropertyDict",
54
                "SamplesAreaDict",
55
                "SamplesScoresDict",
56
                "encode_result_or_sample",
57
                "encode_set",
58
                "encode_decimal",
59
                "_S",
60
                "_SL",
61
                "_R",
62
                ]
63

64
pandas.Series.__module_ = "pandas"  # type: ignore[attr-defined]
1✔
65

66
_S = TypeVar("_S", bound="Sample")
1✔
67
_SL = TypeVar("_SL", bound="SampleList")
1✔
68
_R = TypeVar("_R", bound="Result")
1✔
69

70

71
@prettify_docstrings
1✔
72
class Sample(Dictable):
1✔
73
        """
74
        Represents a sample in a MassHunter CSV file.
75

76
        :param sample_name:
77
        :param sample_type:
78
        :param instrument_name:
79
        :param position:
80
        :param user:
81
        :param acq_method:
82
        :param da_method:
83
        :param irm_cal_status:
84
        :param filename:
85
        :param results:
86
        """
87

88
        def __init__(  # noqa: MAN001  # TODO
1✔
89
                self,
90
                sample_name,
91
                sample_type,
92
                instrument_name,
93
                position,
94
                user,
95
                acq_method,
96
                da_method,
97
                irm_cal_status,
98
                filename,
99
                results=None,
100
        ):
101

102
                self.sample_name = sample_name
1✔
103
                self.sample_type = sample_type
1✔
104
                self.instrument_name = instrument_name
1✔
105
                self.position = position
1✔
106
                self.user = user
1✔
107
                self.acq_method = acq_method
1✔
108
                self.da_method = da_method
1✔
109
                self.irm_cal_status = irm_cal_status
1✔
110
                self.filename = filename
1✔
111

112
                self._results: Dict[float, Result]
1✔
113

114
                if results is None:
1✔
115
                        self._results = {}
1✔
116
                elif isinstance(results, dict):
1✔
117
                        self._results = {}
×
118

119
                        for cpd_no, compound in results.items():
×
120
                                if isinstance(compound, dict):
×
121
                                        self._results[cpd_no] = Result(**compound)
×
122
                                else:
123
                                        self._results[cpd_no] = compound
×
124
                elif isinstance(results, list):
1✔
125
                        self._results = {}
1✔
126

127
                        for compound in results:
1✔
128
                                if isinstance(compound, dict):
1✔
129
                                        tmp_result = Result(**compound)
1✔
130
                                        cpd_no = tmp_result.index
1✔
131
                                        self._results[cpd_no] = tmp_result
1✔
132
                                else:
133
                                        self._results[compound.index] = compound
×
134
                else:
135
                        raise TypeError(f"Unknown type for `results`: {type(results)}")
×
136

137
        def add_result(self, result: "Result") -> None:
1✔
138
                """
139
                Add a result to the sample.
140

141
                :param result:
142
                """
143

144
                self._results[result.index] = result
1✔
145

146
        @property
1✔
147
        def results_list(self) -> List["Result"]:
1✔
148
                """
149
                Returns a list of results in the order in which they were identified.
150

151
                I.e. sorted by the ``Cpd`` value from the csv export.
152

153
                :rtype:
154

155
                .. clearpage::
156
                """
157

158
                results_list = []
1✔
159

160
                for key in sorted(self._results.keys()):
1✔
161
                        results_list.append(self._results[key])
1✔
162

163
                return results_list
1✔
164

165
        def __eq__(self, other) -> bool:  # noqa: MAN001
1✔
166
                if isinstance(other, self.__class__):
1✔
167
                        return (
1✔
168
                                        self.sample_name == other.sample_name and self.sample_type == other.sample_type
169
                                        and self.filename == other.filename and self.acq_method == other.acq_method
170
                                        )
171

NEW
172
                return NotImplemented
×
173

174
        @classmethod
1✔
175
        def from_series(cls: Type[_S], series: pandas.Series) -> _S:
1✔
176
                """
177
                Constuct a :class:`~.Sample` from a :class:`pandas.Series`.
178

179
                :param series:
180
                :return:
181
                """
182

183
                sample_name = series["Sample Name"]
1✔
184
                sample_type = series["Sample Type"]
1✔
185
                filename = series["File"]
1✔
186
                instrument_name = series["Instrument Name"]
1✔
187
                position = series["Position"]
1✔
188
                user = series["User Name"]
1✔
189
                acq_method = series["Acq Method"]
1✔
190
                da_method = series["DA Method"]
1✔
191
                irm_cal_status = series["IRM Calibration status"]
1✔
192

193
                return cls(
1✔
194
                                sample_name,
195
                                sample_type,
196
                                instrument_name,
197
                                position,
198
                                user,
199
                                acq_method,
200
                                da_method,
201
                                irm_cal_status,
202
                                filename,
203
                                )
204

205
        def __repr__(self) -> str:
1✔
206
                return f"Sample({self.sample_name})"
×
207

208
        def to_dict(self) -> Mapping[str, Any]:
1✔
209
                """
210
                Return a dictionary representation of the class.
211
                """
212

213
                return AlphaDict(
1✔
214
                                sample_name=self.sample_name,
215
                                sample_type=self.sample_type,
216
                                instrument_name=self.instrument_name,
217
                                position=self.position,
218
                                user=self.user,
219
                                acq_method=self.acq_method,
220
                                da_method=self.da_method,
221
                                irm_cal_status=self.irm_cal_status,
222
                                filename=self.filename,
223
                                results=self.results_list,
224
                                )
225

226

227
@prettify_docstrings
1✔
228
class Result(Dictable):
1✔
229
        r"""
230
        Represents a Result in a MassHunter CSV file.
231

232
        .. raw:: latex
233

234
                \begin{multicols}{2}
235

236
        :param cas:
237
        :param name:
238
        :param hits:
239
        :param index:
240
        :param formula:
241
        :param score:
242
        :param abundance:
243
        :param height:
244
        :param area:
245
        :param diff_mDa:
246
        :param diff_ppm:
247
        :param rt:
248
        :param start:
249
        :param end:
250
        :param width:
251
        :param tgt_rt:
252
        :param rt_diff:
253
        :param mz:
254
        :param product_mz:
255
        :param base_peak:
256
        :param mass:
257
        :param average_mass:
258
        :param tgt_mass:
259
        :param mining_algorithm:
260
        :param z_count:
261
        :param max_z:
262
        :param min_z:
263
        :param n_ions:
264
        :param polarity:
265
        :param label:
266
        :param flags:
267
        :param flag_severity:
268
        :param flag_severity_code:
269

270
        .. raw:: latex
271

272
                \end{multicols}
273
        """
274

275
        def __init__(  # noqa: MAN001  # TODO
1✔
276
                self,
277
                cas,
278
                name: str,
279
                hits,
280
                index: int = -1,
281
                formula: str = '',
282
                score: float = 0.0,
283
                abundance: float = 0,
284
                height: float = 0,
285
                area: float = 0,
286
                diff_mDa: float = 0.0,
287
                diff_ppm: float = 0.0,
288
                rt: float = 0.0,
289
                start: float = 0.0,
290
                end: float = 0.0,
291
                width: float = 0.0,
292
                tgt_rt: float = 0.0,
293
                rt_diff: float = 0.0,
294
                mz: float = 0.0,
295
                product_mz: float = 0.0,
296
                base_peak: float = 0.0,
297
                mass: float = 0.0,
298
                average_mass: float = 0.0,
299
                tgt_mass: float = 0.0,
300
                mining_algorithm: str = '',
301
                z_count: int = 0,
302
                max_z: int = 0,
303
                min_z: int = 0,
304
                n_ions: int = 0,
305
                polarity: str = '',
306
                label: str = '',
307
                flags: str = '',
308
                flag_severity: str = '',
309
                flag_severity_code: int = 0,
310
        ):
311

312
                # Possible also AL (ID Source) and AM (ID Techniques Applied)
313
                self._cas = cas
1✔
314
                self.name: str = str(name)
1✔
315
                self.hits = hits
1✔
316
                self.formula: str = str(formula)
1✔
317
                self.score: Decimal = Decimal(score)
1✔
318
                self.abundance: float = int(abundance)
1✔
319
                self.height: float = int(height)
1✔
320
                self.area: float = int(area)
1✔
321
                self.diff_mDa: Decimal = Decimal(diff_mDa)
1✔
322
                self.diff_ppm: Decimal = Decimal(diff_ppm)
1✔
323
                self.rt: Decimal = Decimal(rt)
1✔
324
                self.start: Decimal = Decimal(start)
1✔
325
                self.end: Decimal = Decimal(end)
1✔
326
                self.width: Decimal = Decimal(width)
1✔
327
                self.tgt_rt: Decimal = Decimal(tgt_rt)
1✔
328
                self.rt_diff: Decimal = Decimal(rt_diff)
1✔
329
                self.mz: Decimal = Decimal(mz)
1✔
330
                self.product_mz: Decimal = Decimal(product_mz)
1✔
331
                self.base_peak: Decimal = Decimal(base_peak)
1✔
332
                self.mass: Decimal = Decimal(mass)
1✔
333
                self.average_mass: Decimal = Decimal(average_mass)
1✔
334
                self.tgt_mass: Decimal = Decimal(tgt_mass)
1✔
335
                self.mining_algorithm: str = str(mining_algorithm)
1✔
336
                self.z_count: int = int(z_count)
1✔
337
                self.max_z: int = int(max_z)
1✔
338
                self.min_z: int = int(min_z)
1✔
339
                self.n_ions: int = int(n_ions)
1✔
340
                self.polarity: str = str(polarity)
1✔
341
                self.label: str = str(label)
1✔
342
                self.flags: str = str(flags)
1✔
343
                self.flag_severity: str = str(flag_severity)
1✔
344
                self.flag_severity_code: int = int(flag_severity_code)
1✔
345
                self.index: int = index  # Tracks the number of the result in the sample
1✔
346

347
        # "Score (Tgt)",
348
        @classmethod
1✔
349
        def from_series(cls: Type[_R], series: pandas.Series) -> _R:
1✔
350
                """
351
                Consruct a :class:`~.classes.Result` from a :class:`pandas.Series`.
352

353
                :param series:
354

355
                :rtype:
356

357
                .. clearpage::
358
                """
359

360
                cas = series["CAS"]
1✔
361
                name = series["Name"]
1✔
362
                index = series["Cpd"]
1✔
363
                hits = series["Hits"]
1✔
364
                formula = series["Formula"]
1✔
365
                score = series["Score"]
1✔
366
                abundance = series["Abund"]
1✔
367
                height = series["Height"]
1✔
368
                area = series["Area"]
1✔
369
                diff_mDa = series["Diff (Tgt, mDa)"]
1✔
370
                diff_ppm = series["Diff (Tgt, ppm)"]
1✔
371
                rt = series["RT"]
1✔
372
                start = series["Start"]
1✔
373
                end = series["End"]
1✔
374
                width = series["Width"]
1✔
375
                tgt_rt = series["RT (Tgt)"]
1✔
376
                rt_diff = series["RT Diff (Tgt)"]
1✔
377
                mz = series["m/z"]
1✔
378
                product_mz = series["m/z (prod.)"]
1✔
379
                base_peak = series["Base Peak"]
1✔
380
                mass = series["Mass"]
1✔
381
                average_mass = series["Avg Mass"]
1✔
382
                tgt_mass = series["Mass (Tgt)"]
1✔
383
                mining_algorithm = series["Mining Algorithm"]
1✔
384
                z_count = series["Z Count"]
1✔
385
                max_z = series["Max Z"]
1✔
386
                min_z = series["Min Z"]
1✔
387
                n_ions = series["Ions"]
1✔
388
                polarity = series["Polarity"]
1✔
389
                label = series["Label"]
1✔
390
                flags = series["Flags (Tgt)"]
1✔
391
                flag_severity = series["Flag Severity (Tgt)"]
1✔
392
                flag_severity_code = series["Flag Severity Code (Tgt)"]
1✔
393

394
                return cls(
1✔
395
                                cas,
396
                                name,
397
                                hits,
398
                                index,
399
                                formula,
400
                                score,
401
                                abundance,
402
                                height,
403
                                area,
404
                                diff_mDa,
405
                                diff_ppm,
406
                                rt,
407
                                start,
408
                                end,
409
                                width,
410
                                tgt_rt,
411
                                rt_diff,
412
                                mz,
413
                                product_mz,
414
                                base_peak,
415
                                mass,
416
                                average_mass,
417
                                tgt_mass,
418
                                mining_algorithm,
419
                                z_count,
420
                                max_z,
421
                                min_z,
422
                                n_ions,
423
                                polarity,
424
                                label,
425
                                flags,
426
                                flag_severity,
427
                                flag_severity_code,
428
                                )
429

430
        def __repr__(self) -> str:
1✔
431
                return f"Result({self.name}; {self.formula}; {self.rt}; {self.score})"
×
432

433
        def to_dict(self) -> Mapping[str, Any]:
1✔
434
                """
435
                Return a dictionary representation of the class.
436
                """
437

438
                return AlphaDict(
1✔
439
                                cas=self._cas,
440
                                name=self.name,
441
                                hits=self.hits,
442
                                formula=self.formula,
443
                                score=self.score,
444
                                abundance=self.abundance,
445
                                height=self.height,
446
                                area=self.area,
447
                                diff_mDa=self.diff_mDa,
448
                                diff_ppm=self.diff_ppm,
449
                                rt=self.rt,
450
                                start=self.start,
451
                                end=self.end,
452
                                width=self.width,
453
                                tgt_rt=self.tgt_rt,
454
                                rt_diff=self.rt_diff,
455
                                mz=self.mz,
456
                                product_mz=self.product_mz,
457
                                base_peak=self.base_peak,
458
                                mass=self.mass,
459
                                average_mass=self.average_mass,
460
                                tgt_mass=self.tgt_mass,
461
                                mining_algorithm=self.mining_algorithm,
462
                                z_count=self.z_count,
463
                                max_z=self.max_z,
464
                                min_z=self.min_z,
465
                                n_ions=self.n_ions,
466
                                polarity=self.polarity,
467
                                label=self.label,
468
                                flags=self.flags,
469
                                flag_severity=self.flag_severity,
470
                                flag_severity_code=self.flag_severity_code,
471
                                index=self.index,
472
                                )
473

474
        def __eq__(self, other) -> bool:  # noqa: MAN001
1✔
475
                if isinstance(other, str):
×
476
                        return other.casefold() == self.name.casefold()
×
477
                else:
478
                        return NotImplemented
×
479

480

481
class SampleList(List[Sample]):
1✔
482
        """
483
        A list of :class:`mh_utils.csv_parser.classes.Sample` objects.
484
        """
485

486
        @doctools.append_docstring_from(Sample.__init__)
1✔
487
        def add_new_sample(self, *args, **kwargs) -> Sample:  # noqa: PRM002
1✔
488
                """
489
                Add a new sample to the list and return the
490
                :class:`~classes.Sample` object representing it.
491

492
                """  # noqa: D400
493

494
                tmp_sample = Sample(*args, **kwargs)
×
495
                return self.add_sample(tmp_sample)
×
496

497
        def add_sample(self, sample: Sample) -> Sample:
1✔
498
                """
499
                Add a :class:`~.Sample` object to the list.
500

501
                :param sample:
502

503
                :rtype:
504

505
                .. clearpage::
506
                """
507

508
                if sample in self:
1✔
509
                        return self[self.index(sample)]
1✔
510
                else:
511
                        self.append(sample)
1✔
512
                        return sample
1✔
513

514
        # def find_sample(self, sample_name: str) -> Optional[Sample]:
515
        #         if sample_name in self:
516
        #                 return self[self.index(sample_name)]
517
        #         else:
518
        #                 return None
519

520
        def add_sample_from_series(self, series: pandas.Series) -> Sample:
1✔
521
                """
522
                Create a new sample object from a :class:`pandas.series` and add it to the list.
523

524
                :returns: The newly created :class:`~classes.Sample` object.
525

526
                :param series:
527
                """
528

529
                tmp_sample = Sample.from_series(series)
1✔
530
                return self.add_sample(tmp_sample)
1✔
531

532
        def sort_samples(self, key: str, reverse: bool = False) -> None:
1✔
533
                """
534
                Sort the list of :class:`~.Samples` in place.
535

536
                :param key: The name of the property in the sample to sort by.
537
                :param reverse: Whether the list should be sorted in reverse order.
538

539
                :rtype:
540

541
                .. clearpage::
542
                """
543

544
                self.sort(key=lambda samp: getattr(samp, key), reverse=reverse)
×
545

546
        def reorder_samples(self, order_mapping: Dict, key: str = "sample_name") -> None:
1✔
547
                """
548
                Reorder the list of :class:`~.Samples` in place.
549

550
                :param order_mapping: A mapping between sample names and their new position in the list.
551
                        For example:
552

553
                                .. code-block:: python
554

555
                                        order_mapping = {
556
                                                "Propellant 1ug +ve": 0,
557
                                                "Propellant 1mg +ve": 1,
558
                                                "Propellant 1ug -ve": 2,
559
                                                "Propellant 1mg -ve": 3,
560
                                                }
561

562
                :param key: The name of the property in the sample to sort by.
563
                """
564

565
                self.sort(key=lambda s: order_mapping[getattr(s, key)], reverse=True)
×
566

567
        def rename_samples(self, rename_mapping: Dict, key: str = "sample_name") -> None:
1✔
568
                r"""
569
                Rename the samples in the list.
570

571
                :param rename_mapping: A mapping between current sample names and their new names.
572
                :param key: The name of the property in the sample to sort by.
573

574
                Use ``rename_mapping=``\:py:obj:`None` or omit the sample from the ``rename_mapping`` entirely
575
                to leave the name unchanged.
576

577
                For example:
578

579
                .. code-block:: python
580

581
                        rename_mapping = {
582
                                "Propellant 1ug +ve": "Alliant Unique 1µg/L +ESI",
583
                                "Propellant 1mg +ve": "Alliant Unique 1mg/L +ESI",
584
                                "Propellant 1mg -ve": None,
585
                                }
586
                """
587

588
                for sample in self:
×
589
                        if getattr(sample, key) in rename_mapping and rename_mapping[getattr(sample, key)]:
×
590
                                sample.sample_name = rename_mapping.pop(getattr(sample, key))
×
591

592
        def get_areas_and_scores(
1✔
593
                        self,
594
                        compound_name: str,
595
                        include_none: bool = False,
596
                        ) -> Tuple[OrderedDict, OrderedDict]:
597
                """
598
                Returns two dictionaries: one containing sample names and peak areas for the
599
                compound with the given name, the other containing sample names and scores.
600

601
                :param compound_name:
602
                :param include_none: Whether samples where the compound was not found
603
                        should be included in the results.
604
                """  # noqa: D400
605

606
                peak_areas: "OrderedDict[str, Optional[float]]" = OrderedDict()
×
607
                scores: "OrderedDict[str, Optional[Decimal]]" = OrderedDict()
×
608

609
                for sample in self:
×
610
                        for result in sample.results_list:
×
611
                                if result.name == compound_name:
×
612
                                        peak_areas[sample.sample_name] = result.area
×
613
                                        scores[sample.sample_name] = result.score
×
614
                                        break
×
615
                        else:
616
                                if include_none:
×
617
                                        peak_areas[sample.sample_name] = None
×
618
                                        scores[sample.sample_name] = None
×
619

620
                return peak_areas, scores
×
621

622
        def get_retention_times(self, compound_name: str, include_none: bool = False) -> OrderedDict:
1✔
623
                """
624
                Returns a dictionary containing sample names and retention times for the
625
                compound with the given name.
626

627
                :param compound_name:
628
                :param include_none: Whether samples where the compound was not found
629
                        should be included in the results.
630
                """  # noqa: D400
631

632
                times = OrderedDict()
×
633

634
                for sample in self:
×
635
                        for result in sample.results_list:
×
636
                                if result.name == compound_name:
×
637
                                        times[sample.sample_name] = float(result.rt)
×
638
                                        break
×
639
                        else:
640
                                if include_none:
×
641
                                        times[sample.sample_name] = numpy.nan
×
642

643
                return times
×
644

645
        def get_peak_areas(self, compound_name: str, include_none: bool = False) -> OrderedDict:
1✔
646
                """
647
                Returns a dictionary containing sample names and peak areas for the
648
                compound with the given name.
649

650
                :param compound_name:
651
                :param include_none: Whether samples where the compound was not found
652
                        should be included in the results.
653
                """  # noqa: D400
654

655
                return self.get_areas_and_scores(compound_name, include_none)[0]
×
656

657
        def get_areas_for_compounds(
1✔
658
                        self,
659
                        compound_names: Iterable[str],
660
                        include_none: bool = False,
661
                        ) -> "SamplesAreaDict":
662
                """
663
                Returns a dictionary containing sample names and peak areas for the
664
                compounds with the given names.
665

666
                :param compound_names:
667
                :param include_none: Whether samples where none of the specified compounds
668
                        were found should be included in the results.
669
                """  # noqa: D400
670

671
                all_areas, all_scores = self.get_areas_and_scores_for_compounds(compound_names, include_none)
×
672
                return all_areas
×
673

674
        def get_areas_and_scores_for_compounds(
1✔
675
                        self,
676
                        compound_names: Iterable[str],
677
                        include_none: bool = False,
678
                        ) -> Tuple["SamplesAreaDict", "SamplesScoresDict"]:
679
                """
680
                Returns two dictionaries: one containing sample names and peak areas for the
681
                compounds with the given names, the other containing sample names and scores.
682

683
                :param compound_names:
684
                :param include_none: Whether samples where none of the specified compounds
685
                        were found should be included in the results.
686

687
                :rtype:
688

689
                .. clearpage::
690
                """  # noqa: D400
691

692
                tmp_all_areas = SamplesAreaDict()
×
693
                tmp_all_scores = SamplesScoresDict()
×
694

695
                for name in compound_names:
×
696
                        areas = self.get_peak_areas(name, True)
×
697
                        scores = self.get_scores(name, True)
×
698

699
                        for sample_name, area in areas.items():
×
700
                                if sample_name not in tmp_all_areas:
×
701
                                        tmp_all_areas[sample_name] = dict()
×
702
                                        tmp_all_scores[sample_name] = dict()
×
703

704
                                tmp_all_areas[sample_name][name] = area
×
705
                                tmp_all_scores[sample_name][name] = scores[sample_name]
×
706

707
                if include_none:
×
708
                        return tmp_all_areas, tmp_all_scores
×
709

710
                else:
711
                        all_areas = SamplesAreaDict()
×
712
                        all_scores = SamplesScoresDict()
×
713

714
                        for sample_name, compound_areas in tmp_all_areas.items():
×
715
                                if any(list(compound_areas.values())):
×
716
                                        all_areas[sample_name] = compound_areas
×
717
                                        all_scores[sample_name] = tmp_all_scores[sample_name]
×
718

719
                        return all_areas, all_scores
×
720

721
        def get_compounds(self) -> List[str]:
1✔
722
                """
723
                Returns a list containing the names of the compounds present in the samples in alphabetical order.
724
                """
725

726
                compounds = set()
×
727

728
                for sample in self:
×
729
                        for result in sample.results_list:
×
730
                                compounds.add(result.name)
×
731

732
                return sorted(compounds)
×
733

734
        def get_scores(self, compound_name: str, include_none: bool = False) -> OrderedDict:
1✔
735
                """
736
                Returns a dictionary containing sample names and scores for the
737
                compound with the given name.
738

739
                :param compound_name:
740
                :param include_none: Whether samples where the compound was not found
741
                        should be included in the results.
742

743
                :rtype:
744

745
                .. clearpage::
746
                """  # noqa: D400
747

748
                return self.get_areas_and_scores(compound_name, include_none)[1]
×
749

750
        def filter(  # noqa: A003  # pylint: disable=redefined-builtin
1✔
751
                self: _SL,
752
                sample_names: Iterable[str],
753
                key: str = "sample_name",
754
                exclude: bool = False,
755
        ) -> _SL:
756
                """
757
                Filter the list to only contain sample_names whose name is in ``sample_names``.
758

759
                :param sample_names: A list of sample names to include
760
                :param key: The name of the property in the sample to sort by.
761
                :param exclude: If :py:obj:`True`, any sample whose name is in ``sample_names``
762
                        will be excluded from the output, rather than included.
763
                """
764

765
                new_sample_list = self.__class__()
×
766

767
                for sample in self:
×
768
                        if exclude:
×
769
                                if getattr(sample, key) in sample_names:
×
770
                                        continue
×
771
                        else:
772
                                if getattr(sample, key) not in sample_names:
×
773
                                        continue
×
774

775
                        new_sample_list.append(sample)
×
776

777
                return new_sample_list
×
778

779
        @property
1✔
780
        def sample_names(self) -> List[str]:
1✔
781
                """
782
                Returns a list of sample names in the :class:`~.classes.SampleList`.
783
                """
784

785
                return [sample.sample_name for sample in self]
×
786

787
        @classmethod
1✔
788
        def from_json_file(cls: Type[_SL], filename: PathLike, **kwargs) -> _SL:
1✔
789
                r"""
790
                Construct a :class:`~.classes.SampleList` from JSON file.
791

792
                :param filename: The filename of the JSON file.
793
                :param \*\*kwargs: Keyword arguments passed to :meth:`domdf_python_tools.paths.PathPlus.load_json`.
794
                """
795

796
                all_samples = cls()
×
797

798
                for sample in PathPlus(filename).load_json(
×
799
                                json_library=sdjson,  # type: ignore[arg-type]
800
                                **kwargs,
801
                                ):
802
                        all_samples.append(Sample(**sample))
×
803

804
                return all_samples
×
805

806

807
class BaseSamplePropertyDict(OrderedDict):
1✔
808
        """
809
        OrderedDict to store a single property of a set of samples.
810

811
        Keys are the sample names and the values are dictionaries mapping compound names to property values.
812
        """
813

814
        @property
1✔
815
        def sample_names(self) -> List[str]:
1✔
816
                """
817
                Returns a list of sample names in the :class:`~.BaseSamplePropertyDict`.
818
                """
819

820
                return list(self.keys())
×
821

822
        @property
1✔
823
        def n_samples(self) -> int:
1✔
824
                """
825
                Returns the number of samples in the :class:`~.BaseSamplePropertyDict`.
826
                """
827

828
                return len(self.keys())
×
829

830
        @property
1✔
831
        def n_compounds(self) -> int:
1✔
832
                """
833
                Returns the number of compounds in the :class:`~.BaseSamplePropertyDict`.
834
                """
835

836
                for val in self.values():
×
837
                        return len(val)
×
838
                return 0
×
839

840

841
class SamplesAreaDict(BaseSamplePropertyDict):
1✔
842
        """
843
        :class:`collections.OrderedDict` to store area information parsed from MassHunter results CSV files.
844
        """
845

846
        def get_compound_areas(self, compound_name: str) -> List[float]:
1✔
847
                """
848
                Get the peak areas for the given compound in every sample.
849

850
                :param compound_name:
851
                """
852

853
                areas = []
×
854

855
                for sample_name, compound_areas in self.items():
×
856
                        for name, area in compound_areas.items():
×
857
                                if compound_name == name:
×
858
                                        if area is None:
×
859
                                                areas.append(0.0)
×
860
                                        else:
861
                                                areas.append(area)
×
862

863
                return areas
×
864

865

866
class SamplesScoresDict(BaseSamplePropertyDict):
1✔
867
        """
868
        :class:`collections.OrderedDict` to store score information parsed from MassHunter results CSV files.
869
        """
870

871
        def get_compound_scores(self, compound_name: str) -> List[float]:
1✔
872
                """
873
                Get the peak scores for the given compound in every sample.
874

875
                :param compound_name:
876
                """
877

878
                scores = []
×
879

880
                for sample_name, compound_scores in self.items():
×
881
                        for name, score in compound_scores.items():
×
882
                                if compound_name == name:
×
883
                                        if score is None:
×
884
                                                scores.append(0.0)
×
885
                                        else:
886
                                                scores.append(score)
×
887

888
                return scores
×
889

890

891
@sdjson.encoders.register(Sample)
1✔
892
@sdjson.encoders.register(Result)
1✔
893
def encode_result_or_sample(obj: Union[Sample, Result]) -> dict:  # noqa: D103
1✔
894
        return dict(obj)
1✔
895

896

897
@sdjson.encoders.register(set)
1✔
898
def encode_set(obj: set) -> list:  # noqa: D103
1✔
899
        return list(obj)
×
900

901

902
@sdjson.encoders.register(Decimal)
1✔
903
def encode_decimal(obj: Decimal) -> str:  # noqa: D103
1✔
904
        return str(obj)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc