Skip to content

Commit d3e7b9a

Browse files
committed
session part properties in one.alf.path.PureALFPath (issue #208)
1 parent 3266d6f commit d3e7b9a

File tree

3 files changed

+227
-0
lines changed

3 files changed

+227
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
## [Latest](https://github.com/int-brain-lab/ONE/commits/main) [3.2.0]
33
This version adds session part properties to the ALFPath class and turns off save on delete by default.
44

5+
### Added
6+
7+
- session part properties in one.alf.path.PureALFPath (lab, subject, date, sequence)
8+
- with_\* methods in one.alf.path.PureALFPath for replacing session parts
9+
510
### Modified
611

712
- by default, cache tables are not saved when One.\_\_del\_\_ is called

one/alf/path.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,26 @@ def alf_parts(self):
859859
"""tuple of str: the full ALF path parts, with empty strings for missing parts."""
860860
return tuple(p or '' for p in self.parse_alf_path(as_dict=False))
861861

862+
@property
863+
def lab(self):
864+
"""str: The lab part of the ALF path, or an empty str if not present."""
865+
return self.session_parts[0]
866+
867+
@property
868+
def subject(self):
869+
"""str: The subject part of the ALF path, or an empty str if not present."""
870+
return self.session_parts[1]
871+
872+
@property
873+
def date(self):
874+
"""str: The date part of the ALF path, or an empty str if not present."""
875+
return self.session_parts[2]
876+
877+
@property
878+
def sequence(self):
879+
"""str: The number part of the ALF path, or an empty str if not present."""
880+
return self.session_parts[3]
881+
862882
@property
863883
def namespace(self):
864884
"""str: The namespace part of the ALF name, or and empty str if not present."""
@@ -884,6 +904,134 @@ def extra(self):
884904
"""str: The extra part of the ALF name, or and empty str if not present."""
885905
return self.dataset_name_parts[4]
886906

907+
def with_lab(self, lab, strict=False):
908+
"""Return a new path with the ALF lab changed.
909+
910+
Parameters
911+
----------
912+
lab : str
913+
An ALF lab name part to use.
914+
strict : bool, optional
915+
If True, the lab part must be present in the path, otherwise the lab/Subjects/ part
916+
is added if not present.
917+
918+
Returns
919+
-------
920+
PureALFPath
921+
The same file path but with the lab part replaced with the input.
922+
923+
Raises
924+
------
925+
ValueError
926+
The lab name is invalid.
927+
ALFInvalid
928+
The path is not a valid ALF session path, or the lab part is not present in the path
929+
when strict is True.
930+
931+
"""
932+
if not (lab and spec.regex('^{lab}$').match(lab)):
933+
raise ValueError(f'Invalid lab name: {lab}')
934+
if not self.subject or (strict and not self.lab): # FIXME check logic
935+
raise ALFInvalid(str(self))
936+
937+
pattern = spec.regex(SESSION_SPEC)
938+
repl = fr'{lab}/Subjects/\g<subject>/\g<date>/\g<number>'
939+
return self.__class__(pattern.sub(repl, self.as_posix(), count=1))
940+
941+
def with_subject(self, subject):
942+
"""Return a new path with the ALF subject changed.
943+
944+
Parameters
945+
----------
946+
subject : str
947+
An ALF subject name part to use.
948+
949+
Returns
950+
-------
951+
PureALFPath
952+
The same file path but with the subject part replaced with the input.
953+
954+
Raises
955+
------
956+
ValueError
957+
The subject name is invalid.
958+
ALFInvalid
959+
The path is not a valid ALF session path.
960+
961+
"""
962+
if not (subject and spec.regex('^{subject}$').match(subject)):
963+
raise ValueError(f'Invalid subject name: {subject}')
964+
if not self.subject:
965+
raise ALFInvalid(str(self))
966+
967+
pattern = spec.regex('{subject}/{date}/{number}')
968+
repl = fr'{subject}/\g<date>/\g<number>'
969+
return self.__class__(pattern.sub(repl, self.as_posix()), count=1)
970+
971+
def with_date(self, date):
972+
"""Return a new path with the ALF date changed.
973+
974+
Parameters
975+
----------
976+
date : str, datetime.datetime, datetime.date
977+
An ALF date part to use, in YYYY-MM-DD format.
978+
979+
Returns
980+
-------
981+
PureALFPath
982+
The same file path but with the date part replaced with the input.
983+
984+
Raises
985+
------
986+
ValueError
987+
The date is not in YYYY-MM-DD format.
988+
ALFInvalid
989+
The path is not a valid ALF session path.
990+
991+
"""
992+
if date and not isinstance(date, str):
993+
date = str(date)[:10]
994+
if not (date and spec.regex('^{date}$').match(date)):
995+
raise ValueError(f'Invalid date: {date}')
996+
if not self.date:
997+
raise ALFInvalid(str(self))
998+
999+
pattern = spec.regex('{subject}/{date}/{number}')
1000+
repl = fr'\g<subject>/{date}/\g<number>'
1001+
return self.__class__(pattern.sub(repl, self.as_posix()), count=1)
1002+
1003+
def with_sequence(self, number):
1004+
"""Return a new path with the ALF number changed.
1005+
1006+
Parameters
1007+
----------
1008+
number : str, int
1009+
An ALF number part to use, as a string or integer.
1010+
1011+
Returns
1012+
-------
1013+
PureALFPath
1014+
The same file path but with the number part replaced with the input.
1015+
1016+
Raises
1017+
------
1018+
ValueError
1019+
The number is not a valid ALF number.
1020+
ALFInvalid
1021+
The path is not a valid ALF session path.
1022+
1023+
"""
1024+
if isinstance(number, str):
1025+
number = int(number.strip())
1026+
if number is None or not spec.regex('^{number}$').match(str(number)):
1027+
raise ValueError(f'Invalid number: {number}')
1028+
if not self.sequence:
1029+
raise ALFInvalid(str(self))
1030+
1031+
pattern = spec.regex('{subject}/{date}/{number}')
1032+
repl = fr'\g<subject>/\g<date>/{number:03d}'
1033+
return self.__class__(pattern.sub(repl, self.as_posix()), count=1)
1034+
8871035
def with_object(self, obj):
8881036
"""Return a new path with the ALF object changed.
8891037

one/tests/alf/test_alf_path.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Unit tests for the one.alf.path module."""
22
import unittest
33
import tempfile
4+
from datetime import datetime
45
from types import GeneratorType
56
from uuid import uuid4
67
from pathlib import Path, PurePath, PureWindowsPath, PurePosixPath
@@ -496,6 +497,67 @@ def test_with_extension(self):
496497
self.assertRaises(ValueError, self.alfpath.with_extension, '')
497498
self.assertRaises(ALFInvalid, self.alfpath.with_stem('foo').with_extension, 'ext')
498499

500+
def test_with_lab(self):
501+
"""Test for PureALFPath.with_lab method."""
502+
# Test with lab
503+
expected = ALFPath(*self.alfpath.parts[:-8], 'newlab', *self.alfpath.parts[-7:])
504+
self.assertEqual(expected, self.alfpath.with_lab('newlab'))
505+
# Test without lab
506+
alfpath = ALFPath(*self.alfpath.parts[:-8], *self.alfpath.parts[-6:])
507+
self.assertEqual(expected, alfpath.with_lab('newlab'))
508+
# Test strict
509+
self.assertEqual(expected, self.alfpath.with_lab('newlab', strict=True))
510+
self.assertRaises(ALFInvalid, alfpath.with_lab, 'newlab', strict=True)
511+
# Test validation
512+
self.assertRaises(ValueError, self.alfpath.with_lab, '')
513+
self.assertRaises(ValueError, self.alfpath.with_lab, None)
514+
self.assertRaises(ValueError, self.alfpath.with_lab, '#s!@#')
515+
self.assertRaises(ALFInvalid, self.alfpath.relative_to_session().with_lab, 'lab')
516+
517+
def test_with_subject(self):
518+
"""Test for PureALFPath.with_subject method."""
519+
# Test with subject
520+
expected = ALFPath(*self.alfpath.parts[:-6], 'foo', *self.alfpath.parts[-5:])
521+
self.assertEqual(expected, self.alfpath.with_subject('foo'))
522+
# Test without lab (should not depend on Subjects folder)
523+
alfpath = ALFPath(*self.alfpath.parts[:-8], *self.alfpath.parts[-6:])
524+
expected = ALFPath(*alfpath.parts[:-6], 'foo', *alfpath.parts[-5:])
525+
self.assertEqual(expected, alfpath.with_subject('foo'))
526+
# Test validation
527+
self.assertRaises(ValueError, self.alfpath.with_subject, '')
528+
self.assertRaises(ValueError, self.alfpath.with_subject, None)
529+
self.assertRaises(ValueError, self.alfpath.with_subject, '#s!@#')
530+
self.assertRaises(ALFInvalid, self.alfpath.relative_to_session().with_subject, 'subject')
531+
532+
def test_with_date(self):
533+
"""Test for PureALFPath.with_date method."""
534+
# Test with date
535+
expected = ALFPath(*self.alfpath.parts[:-5], '2020-01-02', *self.alfpath.parts[-4:])
536+
self.assertEqual(expected, self.alfpath.with_date('2020-01-02'))
537+
# Test with datetime object
538+
date = datetime.fromisoformat('2020-01-02T00:00:00')
539+
self.assertEqual(expected, self.alfpath.with_date(date))
540+
# Test validation
541+
self.assertRaises(ValueError, self.alfpath.with_date, '')
542+
self.assertRaises(ValueError, self.alfpath.with_date, None)
543+
self.assertRaises(ValueError, self.alfpath.with_date, '6/1/2020')
544+
self.assertRaises(ALFInvalid, self.alfpath.relative_to_session().with_date, '2020-01-02')
545+
546+
def test_with_sequence(self):
547+
"""Test for PureALFPath.with_sequence method."""
548+
# Test with number
549+
expected = ALFPath(*self.alfpath.parts[:-4], '002', *self.alfpath.parts[-3:])
550+
self.assertEqual(expected, self.alfpath.with_sequence(2))
551+
self.assertEqual(expected, self.alfpath.with_sequence('002'))
552+
# Test with zero
553+
self.assertEqual('000', self.alfpath.with_sequence(0).parts[-4])
554+
# Test validation
555+
self.assertRaises(ValueError, self.alfpath.with_sequence, '')
556+
self.assertRaises(ValueError, self.alfpath.with_sequence, None)
557+
self.assertRaises(ValueError, self.alfpath.with_sequence, 'foo')
558+
self.assertRaises(ValueError, self.alfpath.with_sequence, 1e4)
559+
self.assertRaises(ALFInvalid, self.alfpath.relative_to_session().with_sequence, 2)
560+
499561
def test_parts_properties(self):
500562
"""Test the PureALFPath ALF dataset part properties."""
501563
# Namespace
@@ -521,6 +583,18 @@ def test_parts_properties(self):
521583
alfpath = self.alfpath.with_name('_ns_obj.attr_times_bpod.foo.bar.ext')
522584
expected = ('ns', 'obj', 'attr_times', 'bpod', 'foo.bar', 'ext')
523585
self.assertEqual(expected, alfpath.dataset_name_parts)
586+
# Lab
587+
self.assertEqual('labname', self.alfpath.lab)
588+
self.assertEqual('', self.alfpath.relative_to_session().lab)
589+
# Subject
590+
self.assertEqual('subject', self.alfpath.subject)
591+
self.assertEqual('', self.alfpath.relative_to_session().subject)
592+
# Date
593+
self.assertEqual('1900-01-01', self.alfpath.date)
594+
self.assertEqual('', self.alfpath.relative_to_session().date)
595+
# Number
596+
self.assertEqual('001', self.alfpath.sequence)
597+
self.assertEqual('', self.alfpath.relative_to_session().sequence)
524598
# session_parts
525599
self.assertEqual(('labname', 'subject', '1900-01-01', '001'), self.alfpath.session_parts)
526600
alfpath = ALFPath(*self.alfpath.parts[5:])

0 commit comments

Comments
 (0)