Repository navigation
fix: read files included with -r in the requirements.txt parser - #551
Conversation
_parse_internal always reopened the parser's own entry file via self.file_handler, even when it recursed into a file referenced with -r. The referenced file was never read: its -r line was marked as seen and skipped, so its packages were dropped. For a typosquatting scanner this silently hides every dependency pulled in through -r. Read each file from its own path instead. The entry file keeps using the parser's handler (so its existence/emptiness errors are unchanged); each -r include is parsed from its resolved path, and a missing or empty include is logged and skipped.
One- and two-level -r includes are all collected, and a missing -r target is skipped without aborting the parse.
|
/lgtm review |
There was a problem hiding this comment.
🦉 lgtm Review
Score: Nitpicks 🤓
🔍 Summary
This is a great PR that fixes a critical bug where dependencies from included requirements.txt files were silently ignored. The solution is well-structured, extracting the logic for handling includes into a separate method and adding robust error handling for missing files. The added tests are comprehensive and cover the new functionality well.
I've found one minor logical issue in how the file handler is selected, which I've detailed in a line comment. Addressing this will make the implementation more correct and efficient.
Overall, excellent work! With that one change, this PR will be in perfect shape.
More information
- Id:
c73cfb96c23542a28640e64624714d20 - Model:
gemini-2.5-pro - Created at:
2026-10-10T09:54:15.368200+00:00
Usage summary
- Request count:
2 - Request tokens:
16,093 - Response tokens:
10,714 - Total tokens:
26,807
Configuration
-
model:
gemini-2.5-pro -
model_url:
None -
technologies:
('Python',) -
categories:
('Correctness', 'Quality', 'Testing', 'Security') -
exclude:
('*.md', 'uv.lock', 'eval/classified_articles.py') -
additional_context:
() -
publish:
True -
output_format:
pretty -
silent:
False -
ai_retries:
None -
ai_input_tokens_limit:
500000 -
issues_url:
None -
issues_regex:
(?:refs?|closes?|resolves?)[:\s]*((?:#\d+)|(?:#?[A-Z]+-\d+))|(?:fix|feat|docs|style|refactor|perf|test|build|ci)\((?:#(\d+)|#?([A-Z]+-\d+))\)!?: -
issues_platform:
None -
compare:
HEAD
See the 📚 lgtm-ai repository for more information about lgtm.
| # The entry file reuses the parser's own handler (keeping its | ||
| # existence/emptiness errors); every ``-r`` include is read from its | ||
| # own path instead of re-reading the entry file. | ||
| handler = self.file_handler if source == self.file_path else FileHandler(str(source)) |
There was a problem hiding this comment.
🦉 🎯 Correctness
Severity: MEDIUM 🟡
handler = self.file_handler if source == self.file_path else FileHandler(str(source))The comparison source == self.file_path will always evaluate to False because it compares a pathlib.Path object with a string. This leads to a new FileHandler instance being created unnecessarily for the main file, which contradicts the comment's intent to reuse self.file_handler.
To fix this, you should compare two Path objects. For robustness, it's best to compare their resolved absolute paths.
| handler = self.file_handler if source == self.file_path else FileHandler(str(source)) | |
| handler = ( | |
| self.file_handler | |
| if source.resolve() == Path(self.file_path).resolve() | |
| else FileHandler(str(source)) | |
| ) |
More information about this comment
- File:
src/twyn/dependency_parser/parsers/requirements_txt_parser.py - Line:
53 - Relative line:
32 - With suggestion:
Yes - Suggestion ready for replacement:
Yes
There was a problem hiding this comment.
Thanks for the review!
On the line comment about source == self.file_path: both sides are Path objects here. source is converted with source = Path(source) on the line just above, and self.file_path is already a Path (set in AbstractParser.__init__). So for the entry file the comparison evaluates to True and self.file_handler is reused as intended; only -r includes fall through to a fresh FileHandler. I double-checked by running it — for the entry file handler is self.file_handler is True, and the parse result is correct.
That said, I'm happy to switch to source.resolve() == Path(self.file_path).resolve() if you'd prefer it for extra robustness against symlinked or non-normalized paths. Just let me know.
There was a problem hiding this comment.
Hey! No worries, AI did an oopsie as is tradition. The PR looks good! Thanks a lot for the contribution!
I guess this bug has been here since a long time ago; we never catched it cause we don't use requirements files.
What
requirements.txtfiles that pull in other files with-r other.txthad the included packages silently dropped.RequirementsTxtParser._parse_internalresolved the referenced path correctly but then always reopenedself.file_handler(the entry file) instead of the referenced file. The-rtarget was added toseen_filesand skipped on the re-read, so its packages never made it into the result.Why it matters
twyn checks dependency names against typosquats. Any dependency declared in a file reached through
-rwas never parsed, so it was never checked — a silent gap in coverage for a very common requirements layout (e.g.requirements.txtwith-r requirements-dev.txt).Reproduction (before this change)
RequirementsTxtParser("requirements.txt").parse()returned{"flask"}instead of{"flask", "requests", "colorama"}.Change
-rinclude is read from its resolved path.-rtarget is logged and skipped instead of aborting the parse._parse_includehelper.Tests
test_parse_requirements_txt_file_with_includes— one- and two-level-rincludes are all collected.test_parse_requirements_txt_file_skips_missing_include— a missing-rtarget is skipped, the rest is still parsed.pytest testsis green.ruff format --check,ruff checkandmypypass on the changed files.