-
Notifications
You must be signed in to change notification settings - Fork 0
GH45 - add xts alias rework #52
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
base: develop
Are you sure you want to change the base?
Conversation
|
I have read the CLA Document and I hereby sign the CLA 1 out of 2 committers have signed the CLA. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR significantly refactors the XTS alias management system to support multiple input types (URLs, files, and directories) with improved caching and a cleaner CLI interface.
Key changes:
- Rewrote alias management to handle URLs, single files, and directories of .xts files
- Changed CLI from
xts aliassubcommands toxts --aliasflag-based interface - Simplified the main XTS class by removing auto-discovery of .xts files in favor of explicit alias resolution
- Introduced hash-based cache naming to avoid file collisions
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| src/xts_core/xts_alias.py | Complete rewrite with new helper functions, improved caching logic, directory support, and CLI handler |
| src/xts_core/xts.py | Removed old alias subcommand parsing, removed auto-discovery, simplified to use alias-only workflow with new help system |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ensure_dirs() | ||
| filename = hashlib.sha256(url.encode()).hexdigest() + ".xts" | ||
| path = os.path.join(CACHE_DIR, filename) | ||
|
|
||
| if not os.path.exists(path): | ||
| print(f"Fetching remote .xts config from: {url}") | ||
| r = requests.get(url) | ||
| r.raise_for_status() | ||
| with open(path, "w", encoding="utf-8") as f: | ||
| f.write(r.text) | ||
| # filename = hashlib.sha256(url.encode()).hexdigest() + ".xts" | ||
| filename = Path(urlparse(url).path).name | ||
| if not filename: | ||
| raise ValueError(f"URL does not contain a filename: {url}") | ||
|
|
||
| cached_path = os.path.join(CACHE_DIR, filename) | ||
|
|
||
| print(f"Fetching remote .xts config from: {url}") | ||
| r = requests.get(url) | ||
| r.raise_for_status() | ||
|
|
||
| with open(cached_path, "w", encoding="utf-8") as f: | ||
| f.write(r.text) | ||
|
|
||
| return path | ||
| return cached_path |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fetch_url_to_cache function now always re-downloads the file even if it already exists in cache (the if not os.path.exists(path) check was removed). This could lead to unnecessary network requests and slower performance. Consider adding back a check to skip downloading if the file already exists, or add a force parameter to control this behavior.
src/xts_core/xts_alias.py
Outdated
|
|
||
| def _cache_name_for_local_file(src: Path) -> str: | ||
| """ | ||
| Produce a stable cache filename for a local file, avoiding collisoins |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spelling error: "collisoins" should be "collisions".
| Produce a stable cache filename for a local file, avoiding collisoins | |
| Produce a stable cache filename for a local file, avoiding collisions |
src/xts_core/xts_alias.py
Outdated
| cache_name = _cache_name_for_local_file(src) | ||
| dst = Path(CACHE_DIR) / cache_name | ||
|
|
||
| shutil.copy2(src, dst) #ovewrite to refresh |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spelling error: "#ovewrite" should be "# overwrite" (missing space after #).
| shutil.copy2(src, dst) #ovewrite to refresh | |
| shutil.copy2(src, dst) # overwrite to refresh |
src/xts_core/xts_alias.py
Outdated
| if remove_name is not None: | ||
| removed = remove_alias(remove_name) | ||
| if removed: | ||
| print(f"Removed alias: {remove_name}") | ||
| return 0 | ||
| print(f"Alias not found: {remove_name}") | ||
| return 2 |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable 'removed' is assigned the return value of remove_alias(), but remove_alias() returns None. This check will always fail. The logic should be revised to check whether the alias exists before attempting removal, or the remove_alias function should return a boolean.
src/xts_core/xts_alias.py
Outdated
| def remove_alias(name: str) -> None: | ||
| """ | ||
| Remove an alias if it exists. | ||
| """ | ||
| aliases = load_aliases() | ||
| if name in aliases: | ||
| del aliases[name] | ||
| with open(ALIAS_FILE, "w") as f: | ||
| json.dump(aliases, f, indent=2) | ||
| save_aliases(aliases) |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remove_alias function returns None in all cases, but the calling code checks if the return value is truthy (line 340). This will always evaluate to False, making the success message unreachable. The function should return True when an alias is removed or False when it doesn't exist.
src/xts_core/xts.py
Outdated
| if not resolved_xts_path: | ||
| error(f"Alias '{alias_name}' not found. Add it via: xts --alias <path|url|dir> [--name <name>]") | ||
|
|
||
| # laod xts config remove alias name from argv before parsing |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spelling error: "laod" should be "load".
| # laod xts config remove alias name from argv before parsing | |
| # load xts config remove alias name from argv before parsing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
src/xts_core/xts_alias.py
Outdated
| from xts_core import utils | ||
|
|
||
| try: | ||
| from .xts_arg_parser import XTSArgumentParser |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra space between 'from' and '.xts_arg_parser'. Should be 'from .xts_arg_parser import XTSArgumentParser'.
| from .xts_arg_parser import XTSArgumentParser | |
| from .xts_arg_parser import XTSArgumentParser |
src/xts_core/xts_alias.py
Outdated
|
|
||
| def _cache_name_for_local_file(src: Path) -> str: | ||
| """ | ||
| Produce a stable cache filename for a local file, avoiding collisoins |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected spelling of 'collisoins' to 'collisions'.
| Produce a stable cache filename for a local file, avoiding collisoins | |
| Produce a stable cache filename for a local file, avoiding collisions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 20 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| os.makedirs(CACHE_DIR, exist_ok=True) | ||
| os.makedirs(os.path.dirname(ALIAS_FILE), exist_ok=True) | ||
|
|
||
| # move these to utils |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 60 "move these to utils" suggests these helper functions should be relocated. If the intention is to move _is_url to the existing utils.is_url function, this creates duplication. The existing utils.is_url uses a simpler string startswith check, while _is_url uses urlparse with more robust validation. Consider consolidating these into a single implementation in utils.py or removing the comment if the functions should remain here.
| # move these to utils | |
| # Local helper functions; _is_url intentionally differs from utils.is_url | |
| # (uses urllib.parse.urlparse for stricter URL validation). |
| action='store', | ||
| default=None, | ||
| help='URI of xts file to add alias of', | ||
| nargs=argparse.OPTIONAL) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nargs=argparse.OPTIONAL should be nargs='?' for an optional positional argument in argparse. argparse.OPTIONAL is not a valid constant; the correct value is the string '?'.
| nargs=argparse.OPTIONAL) | |
| nargs='?') |
| cache_name = _cache_name_for_local_file(src) | ||
| dst = Path(CACHE_DIR) / cache_name | ||
|
|
||
| shutil.copy2(src, dst) #overwrite to refresh |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline comment shows lack of proper spacing around the '#' character. According to PEP 8, there should be at least two spaces before an inline comment and one space between the '#' and the comment text. The comment should be: '# overwrite to refresh'
| shutil.copy2(src, dst) #overwrite to refresh | |
| shutil.copy2(src, dst) # overwrite to refresh |
| return True | ||
| return False | ||
|
|
||
| def resolve_alias_to_xts_path(alias_name: str) -> str | None: |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return type annotation 'str | None' uses the Python 3.10+ union syntax. If the project needs to support Python 3.9 or earlier, this should use 'Optional[str]' from the typing module instead. Check the project's Python version requirements to ensure compatibility.
| remaining_args.append(args.alias) | ||
| xts_alias.run_alias_builtin(remaining_args) | ||
| raise SystemExit(0) | ||
| resolved_xts_path = xts_alias.resolve_alias_to_xts_path(args.alias) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The resolved_xts_path may be None if the alias is not found in resolve_alias_to_xts_path. Setting self.xts_config to None will cause an error in the xts_config setter at line 130 ("xts config specified does not exist"). Consider adding explicit validation and providing a more helpful error message to the user about the unknown alias.
| resolved_xts_path = xts_alias.resolve_alias_to_xts_path(args.alias) | |
| resolved_xts_path = xts_alias.resolve_alias_to_xts_path(args.alias) | |
| if resolved_xts_path is None: | |
| # Provide a clear error when the alias cannot be resolved instead of | |
| # passing None into the xts_config setter. | |
| error( | |
| f'Unknown alias "{args.alias}". ' | |
| 'Use "xts --alias list" to see available aliases.' | |
| ) | |
| raise SystemExit(1) |
| cache_name = _cache_name_for_local_file(src) | ||
| dst = Path(CACHE_DIR) / cache_name | ||
|
|
||
| shutil.copy2(src, dst) #overwrite to refresh |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment indicates 'overwrite to refresh', but shutil.copy2 preserves file metadata including modification times from the source. If the intention is to always refresh with the latest source content, this is correct. However, if modification times are important for cache invalidation logic, consider documenting this behavior more explicitly or using shutil.copy instead.
| shutil.copy2(src, dst) #overwrite to refresh | |
| shutil.copy(src, dst) # overwrite to refresh cache content and metadata |
| filename = Path(urlparse(url).path).name | ||
| if not filename: | ||
| raise ValueError(f"URL does not contain a filename: {url}") | ||
|
|
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commented-out line suggests the previous implementation used a SHA-256 hash of the URL as the filename. The new implementation uses the actual filename from the URL path. This change creates a potential security issue: filenames from URLs could contain path traversal characters (e.g., "../evil.xts") or other problematic characters. Consider sanitizing the filename or validating that it doesn't contain directory separators before using it.
| # Validate that the derived filename cannot be used for path traversal. | |
| if filename in (".", ".."): | |
| raise ValueError(f"URL contains an invalid filename: {url}") | |
| for sep in (os.sep, os.path.altsep): | |
| if sep and sep in filename: | |
| raise ValueError(f"URL contains an invalid filename: {url}") |
| sys.exit(sorted(exit_code)[-1]) | ||
| except Exception as e: | ||
| error( | ||
| 'An unrecognised command caused an error\n\n' |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message uses 'An unrecognised command' but the word is misspelled. It should be 'An unrecognized command' (American) or 'An unrecognised command' (British). The codebase appears to use American spelling based on other comments, so 'unrecognized' is preferred.
| def _parse_first_arg(self): | ||
| """ | ||
| Parse CLI arguments and set up argparse for all commands. | ||
| The first argument must be either: | ||
| - a built-in option starting with '--' (currently supported: --alias) | ||
| - an alias name (resolved via ~/.xts/aliases.json to an .xts file path) | ||
| Direct .xts file usage from cwd or as the first argument is not supported. |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring states "Direct .xts file usage from cwd or as the first argument is not supported." This is a breaking change from the previous behavior where users could run 'xts file.xts command'. This should be documented in the module docstring or changelog, and error messages should clearly guide users to use aliases instead.
| except: | ||
| from xts_core import utils | ||
|
|
||
| try: | ||
| from .xts_arg_parser import XTSArgumentParser | ||
| except: |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except block directly handles BaseException.
| except: | |
| from xts_core import utils | |
| try: | |
| from .xts_arg_parser import XTSArgumentParser | |
| except: | |
| except ImportError: | |
| from xts_core import utils | |
| try: | |
| from .xts_arg_parser import XTSArgumentParser | |
| except ImportError: |
No description provided.