Conversation
cottoncandy previously supported a single set of connection settings
(keys, endpoint, bucket) split across the [login]/[basic]/[gdrive]
config sections. This adds named profiles so users can switch between
accounts/endpoints/buckets at instantiation time:
cci = cc.get_interface(profile='mylab')
- options.py: add get_profile()/list_profiles() that resolve a
[profile:NAME] section over the base sections, with `inherits` for
profile inheritance (multi-level, with cycle detection).
- __init__.py: get_interface()/get_browser() accept profile=; the
credential/bucket/endpoint/backend arguments default to None and are
filled from the resolved profile, so explicit arguments still win.
- defaults.cfg: document profiles and add an explicit default backend key.
- tests: unit tests for profile resolution, inheritance, error cases, and
get_interface precedence (no network).
- README: document profiles and inheritance.
Fully backward compatible: omitting profile= reproduces the current
behavior and existing configuration files keep working unchanged.
https://claude.ai/code/session_01UEkty8EbGpn3mWcwtcSwdw
There was a problem hiding this comment.
Code Review
This pull request introduces configuration profiles to cottoncandy, allowing users to define named profiles with inheritance to easily switch between different accounts, endpoints, or buckets. The review feedback highlights several robust error-handling improvements regarding missing configuration values. Specifically, the reviewer recommends handling None values gracefully when checking for missing S3 credentials in get_interface and get_browser, providing fallback defaults for Google Drive secrets and credentials to avoid TypeError in os.path.join, and safely handling None values before passing them to string2bool.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| ACCESS_KEY = os.path.join(options.userdir, settings['secrets']) | ||
| SECRET_KEY = os.path.join(options.userdir, settings['credentials']) |
There was a problem hiding this comment.
If settings['secrets'] or settings['credentials'] is None (which can happen if they are missing from the configuration or profile), calling os.path.join with a None argument will raise a TypeError: join() argument must be str or bytes, not 'NoneType'. We should provide fallback defaults to prevent runtime crashes.
| ACCESS_KEY = os.path.join(options.userdir, settings['secrets']) | |
| SECRET_KEY = os.path.join(options.userdir, settings['credentials']) | |
| secrets_file = settings['secrets'] or 'client_secrets.json' | |
| creds_file = settings['credentials'] or 'credentials.txt' | |
| ACCESS_KEY = os.path.join(options.userdir, secrets_file) | |
| SECRET_KEY = os.path.join(options.userdir, creds_file) |
References
- If a configuration key is missing and its absence implies that a dependent operation should be skipped, handle the missing key gracefully (e.g., by using a fallback value) to avoid execution errors.
There was a problem hiding this comment.
this shouldn't happen given these keys exist in the default config
| if force_bucket_creation is None: | ||
| force_bucket_creation = string2bool(settings['force_bucket_creation']) |
There was a problem hiding this comment.
If settings['force_bucket_creation'] is None (e.g., if it is missing from the configuration or profile), passing None to string2bool may raise an AttributeError if string2bool expects a string and calls .lower() on it. We should handle None gracefully by falling back to False.
| if force_bucket_creation is None: | |
| force_bucket_creation = string2bool(settings['force_bucket_creation']) | |
| if force_bucket_creation is None: | |
| val = settings['force_bucket_creation'] | |
| force_bucket_creation = string2bool(val) if val is not None else False |
References
- If a configuration key is missing and its absence implies that a dependent operation should be skipped, handle the missing key gracefully (e.g., by using a fallback value) to avoid execution errors.
There was a problem hiding this comment.
this should happen given it should be in the default config
There was a problem hiding this comment.
Pull request overview
This PR introduces named configuration profiles ([profile:NAME]) so callers can select connection identity (credentials/endpoint/bucket/backend) at get_interface() instantiation time, with optional multi-level inheritance and fallbacks to the existing base [login]/[basic]/[gdrive] sections.
Changes:
- Add profile discovery and resolution (including inheritance + cycle detection) in
cottoncandy.options. - Update
get_interface()/get_browser()to acceptprofile=and resolve settings at call time (so explicit kwargs still override). - Add documentation + defaults updates and unit tests for profile behavior and precedence.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents the new profiles feature and inheritance syntax for end users. |
| cottoncandy/tests/test_options.py | Adds unit tests for profile resolution, inheritance, error cases, and get_interface precedence. |
| cottoncandy/options.py | Implements list_profiles() / get_profile() with inheritance chain resolution and cycle detection. |
| cottoncandy/defaults.cfg | Documents profiles and sets an explicit default backend = s3 in [basic]. |
| cottoncandy/init.py | Adds profile= support to factory functions and defers config resolution to instantiation time. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ACCESS_KEY in [False, "False"] or SECRET_KEY in [False, "False"]: | ||
| ACCESS_KEY, SECRET_KEY = get_keys() | ||
| elif backend == 'gdrive': | ||
| ACCESS_KEY = os.path.join(options.userdir, options.config.get('gdrive', 'secrets')) | ||
| SECRET_KEY = os.path.join(options.userdir, options.config.get('gdrive', 'credentials')) | ||
| ACCESS_KEY = os.path.join(options.userdir, settings['secrets']) | ||
| SECRET_KEY = os.path.join(options.userdir, settings['credentials']) |
There was a problem hiding this comment.
this is the gdrive backend behavior, but also we don't really use the gdrive backend anymore
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
Add in multiple profiles in the config fig to specify different connections, and allows user to instantiate client objects based on the saved profile. The changes are backwards-compatible with existing config files.
A profile is a single
[profile:NAME]section in the config file that can have any of the following entries:access_keysecret_keyendpoint_urldefault_bucketsignature_versionforce_bucket_creationbackendbackend = gdrive)secrets/credentials.Anything a profile omits falls back to the base
[login]/[basic]/[gdrive]sections, so this maintins compatibility with existing configs.Profiles can inherit from another with the entry
inherits; a child only specifies the keys it overrides:lab-scratchwill then use the same access key, secret key, and endpoint asmylabResolution / precedence for when configs are available at multiple places
In order of decreasing precedence:
Changes
cottoncandy/options.py: add logic to read profiles from config filecottoncandy/__init__.py:get_interface()/get_browser()adds new argumentprofile=to allow user to specify config at instantiationcottoncandy/defaults.cfg: adds in profilescottoncandy/tests/test_options.py: add unit testsREADME.md: new "Profiles" section.Testing
tests all pass
https://claude.ai/code/session_01UEkty8EbGpn3mWcwtcSwdw
Generated by Claude Code