diff --git a/CHANGELOG.md b/CHANGELOG.md index 7302652f..dc300344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Also adds a new ALFPath class to replace alf path functions. - ALF cache table generation has lower memory footprint - setup in silent mode now uses defaults if base url matches default one - bugfix: error downloading from http server with keep_uuids=True +- one.alf.spec.readableALF and one.alf.spec._dromedary preserve plural acronyms, e.g. 'ROIs' ### Added diff --git a/one/alf/spec.py b/one/alf/spec.py index 6fa018c3..d20d0a26 100644 --- a/one/alf/spec.py +++ b/one/alf/spec.py @@ -246,13 +246,14 @@ def _dromedary(string) -> str: >>> _dromedary('motion_energy') == 'motionEnergy' >>> _dromedary('passive_RFM') == 'passive RFM' >>> _dromedary('FooBarBaz') == 'fooBarBaz' + >>> _dromedary('mpci ROIs') == 'mpciROIs' See Also -------- readableALF """ def _capitalize(x): - return x if x.isupper() else x.capitalize() + return x if re.match(r'^[A-Z]+s?$', x) else x.capitalize() if not string: # short circuit on None and '' return string first, *other = re.split(r'[_\s]', string) @@ -260,7 +261,7 @@ def _capitalize(x): # Already camel/Pascal case, ensure first letter lower case return first[0].lower() + first[1:] # Convert to camel case, preserving all-uppercase elements - first = first if first.isupper() else first.casefold() + first = first if re.match(r'^[A-Z]+s?$', first) else first.lower() return ''.join([first, *map(_capitalize, other)]) @@ -486,10 +487,10 @@ def readableALF(name: str, capitalize: bool = False) -> str: """ words = [] i = 0 - matches = re.finditer(r'[A-Z](?=[a-z0-9])|(?<=[a-z0-9])[A-Z]', name) + matches = re.finditer(r'[A-Z](?=[a-rt-z0-9])|(?<=[a-z0-9])[A-Z]', name) for j in map(re.Match.start, matches): words.append(name[i:j]) i = j words.append(name[i:]) - display_str = ' '.join(map(lambda s: s if s.isupper() else s.lower(), words)) + display_str = ' '.join(map(lambda s: s if re.match(r'^[A-Z]+s?$', s) else s.lower(), words)) return display_str[0].upper() + display_str[1:] if capitalize else display_str diff --git a/one/api.py b/one/api.py index c77f146f..28a1c09a 100644 --- a/one/api.py +++ b/one/api.py @@ -1926,13 +1926,13 @@ def list_aggregates(self, relation: str, identifier: str = None, r'^[\w\/]+(?=aggregates\/)', '', n=1, regex=True) # The relation is the first part after 'aggregates', i.e. the second part records['relation'] = records['rel_path'].map( - lambda x: x.split('aggregates')[-1].split('/')[1].lower()) - records = records[records['relation'] == relation.lower()] + lambda x: x.split('aggregates')[-1].split('/')[1].casefold()) + records = records[records['relation'] == relation.casefold()] def path2id(p) -> str: """Extract identifier from relative path.""" parts = alfiles.rel_path_parts(p)[0].split('/') - idx = list(map(str.lower, parts)).index(relation.lower()) + 1 + idx = list(map(str.casefold, parts)).index(relation.casefold()) + 1 return '/'.join(parts[idx:]) records['identifier'] = records['rel_path'].map(path2id) diff --git a/one/params.py b/one/params.py index 842aa05f..3d9e12b0 100644 --- a/one/params.py +++ b/one/params.py @@ -153,7 +153,7 @@ def setup(client=None, silent=False, make_default=None, username=None, cache_dir if par[k] and len(par[k]) >= 2 and par[k][0] in quotes and par[k][-1] in quotes: warnings.warn('Do not use quotation marks with input answers', UserWarning) ans = input('Strip quotation marks from response? [Y/n]:').strip() or 'y' - if ans.lower()[0] == 'y': + if ans.casefold()[0] == 'y': par[k] = par[k].strip(quotes) if k == 'ALYX_URL': client = par[k] @@ -185,17 +185,17 @@ def setup(client=None, silent=False, make_default=None, username=None, cache_dir answer = input( 'Warning: the directory provided is already a cache for another URL. ' 'This may cause conflicts. Would you like to change the cache location? [Y/n]') - if answer and answer[0].lower() == 'n': + if answer and answer[0].casefold() == 'n': break cache_dir = input(prompt) or cache_dir # Prompt for another directory if make_default is None: answer = input('Would you like to set this URL as the default one? [Y/n]') - make_default = (answer or 'y')[0].lower() == 'y' + make_default = (answer or 'y')[0].casefold() == 'y' # Verify setup pars answer = input('Are the above settings correct? [Y/n]') - if answer and answer.lower()[0] == 'n': + if answer and answer.casefold()[0] == 'n': print('SETUP ABANDONED. Please re-run.') return par_current else: diff --git a/one/registration.py b/one/registration.py index ea955349..a4fb06b7 100644 --- a/one/registration.py +++ b/one/registration.py @@ -73,7 +73,7 @@ def get_dataset_type(filename, dtypes): if dt.name == obj_attr: dataset_types.append(dt) # Check whether pattern matches filename - elif fnmatch(filename.name.lower(), dt.filename_pattern.lower()): + elif fnmatch(filename.name.casefold(), dt.filename_pattern.casefold()): dataset_types.append(dt) n = len(dataset_types) if n == 0: diff --git a/one/remote/aws.py b/one/remote/aws.py index cc0a07ff..809a70df 100644 --- a/one/remote/aws.py +++ b/one/remote/aws.py @@ -211,9 +211,9 @@ def get_s3_from_alyx(alyx, repo_name=REPO_DEFAULT): returned resource will use an unsigned signature. """ session_keys, bucket_name = get_aws_access_keys(alyx, repo_name) - no_creds = not any(filter(None, (v for k, v in session_keys.items() if 'key' in k.lower()))) + no_creds = not any(filter(None, (v for k, v in session_keys.items() if 'key' in k.casefold()))) session = boto3.Session(**session_keys) - if no_creds and 'public' in bucket_name.lower(): + if no_creds and 'public' in bucket_name.casefold(): config = Config(signature_version=UNSIGNED) else: config = None diff --git a/one/remote/globus.py b/one/remote/globus.py index 398f7880..06d62bc8 100644 --- a/one/remote/globus.py +++ b/one/remote/globus.py @@ -241,7 +241,7 @@ def get_token(client_id, refresh_tokens=True): fields = ('refresh_token', 'access_token', 'expires_at_seconds') print('To get a new token, go to this URL and login: {0}'.format(authorize_url)) auth_code = input('Enter the code you get after login here (press "c" to cancel): ').strip() - if auth_code and auth_code.lower() != 'c': + if auth_code and auth_code.casefold() != 'c': token_response = client.oauth2_exchange_code_for_tokens(auth_code) globus_transfer_data = token_response.by_resource_server['transfer.api.globus.org'] return {k: globus_transfer_data.get(k) for k in fields} diff --git a/one/tests/alf/test_alf_spec.py b/one/tests/alf/test_alf_spec.py index 72e82153..9bf65443 100644 --- a/one/tests/alf/test_alf_spec.py +++ b/one/tests/alf/test_alf_spec.py @@ -128,6 +128,7 @@ def test_dromedary(self): self.assertEqual(alf_spec._dromedary('passive_RFM'), 'passiveRFM') self.assertEqual(alf_spec._dromedary('ROI Motion Energy'), 'ROIMotionEnergy') self.assertEqual(alf_spec._dromedary(''), '') + self.assertEqual(alf_spec._dromedary('mpci ROIs'), 'mpciROIs') def test_readable_ALF(self): """Test for one.alf.spec.readableALF function.""" @@ -135,6 +136,7 @@ def test_readable_ALF(self): self.assertEqual(alf_spec.readableALF('ROIMotion'), 'ROI motion') self.assertEqual(alf_spec.readableALF('blueChipTime'), 'blue chip time') self.assertEqual(alf_spec.readableALF('someROIDataset'), 'some ROI dataset') + self.assertEqual(alf_spec.readableALF('someROIsDataset'), 'some ROIs dataset') self.assertEqual(alf_spec.readableALF('fooBAR'), 'foo BAR') self.assertEqual(alf_spec.readableALF('fooBAR', capitalize=True), 'Foo BAR') diff --git a/one/tests/test_one.py b/one/tests/test_one.py index 9a98891d..661d8c01 100644 --- a/one/tests/test_one.py +++ b/one/tests/test_one.py @@ -1972,14 +1972,14 @@ def test_setup(self): url = TEST_DB_1['base_url'] def mock_input(prompt): - if prompt.lower().startswith('warning'): + if prompt.casefold().startswith('warning'): if not getattr(mock_input, 'conflict_warn', False): # Checks both responses mock_input.conflict_warn = True return 'y' return 'n' - elif 'download cache' in prompt.lower(): + elif 'download cache' in prompt.casefold(): return Path(self.tempdir.name).joinpath('downloads').as_posix() - elif 'url' in prompt.lower(): + elif 'url' in prompt.casefold(): return url else: return 'mock_input' diff --git a/one/tests/test_params.py b/one/tests/test_params.py index abd03d69..7489879c 100644 --- a/one/tests/test_params.py +++ b/one/tests/test_params.py @@ -28,9 +28,9 @@ def setUp(self) -> None: def _mock_input(self, prompt, **kwargs): """Stub function for builtins.input""" - if prompt.lower().startswith('warning'): + if prompt.casefold().startswith('warning'): return 'n' - elif 'url' in prompt.lower(): + elif 'url' in prompt.casefold(): return self.url else: for k, v in kwargs.items(): diff --git a/one/util.py b/one/util.py index 149a2667..87f71036 100644 --- a/one/util.py +++ b/one/util.py @@ -472,11 +472,11 @@ def autocomplete(term, search_terms) -> str: """ Validate search term and return complete name, e.g. autocomplete('subj') == 'subject'. """ - term = term.lower() + term = term.casefold() # Check if term already complete if term in search_terms: return term - full_key = (x for x in search_terms if x.lower().startswith(term)) + full_key = (x for x in search_terms if x.casefold().startswith(term)) key_ = next(full_key, None) if not key_: raise ValueError(f'Invalid search term "{term}", see `one.search_terms()`') diff --git a/one/webclient.py b/one/webclient.py index 215a4f0e..de795d39 100644 --- a/one/webclient.py +++ b/one/webclient.py @@ -102,7 +102,7 @@ def wrapper_decorator(alyx_client, *args, expires=None, clobber=False, **kwargs) The REST response JSON either from cached file or directly from remote. """ expires = expires or alyx_client.default_expiry - mode = (alyx_client.cache_mode or '').lower() + mode = (alyx_client.cache_mode or '').casefold() if args[0].__name__ != mode and mode != '*': return method(alyx_client, *args, **kwargs) # Check cache