Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

str.lower -> str.casefold; support for plural acronyms (issue #80) #166

Merged
merged 1 commit into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 5 additions & 4 deletions one/alf/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,21 +246,22 @@ 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)
if len(other) == 0:
# 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)])


Expand Down Expand Up @@ -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
6 changes: 3 additions & 3 deletions one/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions one/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion one/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions one/remote/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion one/remote/globus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions one/tests/alf/test_alf_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,15 @@ 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."""
self.assertEqual(alf_spec.readableALF('DAQData'), 'DAQ data')
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')

Expand Down
6 changes: 3 additions & 3 deletions one/tests/test_one.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions one/tests/test_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
4 changes: 2 additions & 2 deletions one/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()`')
Expand Down
2 changes: 1 addition & 1 deletion one/webclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading