@@ -227,7 +227,6 @@ def test_main_package_file_permission_error(tmp_path, capsys):
227227 package_file = tmp_path / "packages.txt"
228228 package_file .write_text ("packages/pkg_a" )
229229
230- import sys
231230 test_args = ["version_scanner.py" , "-d" , "python" , "-v" , "3.7" , "--package-file" , str (package_file )]
232231
233232 real_open = open
@@ -246,7 +245,6 @@ def side_effect(file, *args, **kwargs):
246245 captured = capsys .readouterr ()
247246 assert "Error: Permission denied reading package file" in captured .err
248247def test_main_package_file_not_found (capsys ):
249- import sys
250248 test_args = ["version_scanner.py" , "-d" , "python" , "-v" , "3.7" , "--package-file" , "non_existent_file.txt" ]
251249
252250 with patch ("sys.argv" , test_args ):
@@ -323,7 +321,6 @@ def test_main_loads_ignore_from_script_dir(mock_scan, mock_load_ignore):
323321 mock_load_ignore .return_value = []
324322 mock_scan .return_value = []
325323
326- import sys
327324 test_args = ["version_scanner.py" , "-d" , "python" , "-v" , "3.7" ]
328325
329326 with mock .patch ('sys.argv' , test_args ):
@@ -339,7 +336,8 @@ def test_main_loads_ignore_from_script_dir(mock_scan, mock_load_ignore):
339336
340337
341338try :
342- import googleapiclient
339+ # Ruff linter F401: Imported solely to detect Google API Client library presence for test skipping
340+ import googleapiclient # noqa: F401
343341 HAS_GOOGLE_API = True
344342except ImportError :
345343 HAS_GOOGLE_API = False
@@ -407,8 +405,8 @@ def test_regex_examples_from_config():
407405
408406 rules_list = config .get ("rules" , [])
409407
410- # Variables for interpolation (simulate Python 3.7)
411- vars = {
408+ # Base variables for interpolation (simulate target version 3.7)
409+ base_vars = {
412410 "major" : "3" ,
413411 "minor" : "7" ,
414412 "version" : "3.7" ,
@@ -424,6 +422,11 @@ def test_regex_examples_from_config():
424422 if not examples or not templates :
425423 continue
426424
425+ # Resolve target dependency name based on applies_to metadata, falling back to protobuf
426+ applies_to = rule_group .get ("applies_to" , [])
427+ dep_name = applies_to [0 ] if applies_to else "protobuf"
428+ vars = {** base_vars , "name" : dep_name }
429+
427430 compiled_patterns = []
428431 for template in templates :
429432 try :
@@ -638,39 +641,67 @@ def test_format_for_raw_csv_handles_empty_line_number():
638641
639642def test_format_for_raw_csv ():
640643 match = {
644+ "file_name" : "setup.py" ,
641645 "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
642646 "repo_path" : "packages/pkg_a/setup.py" ,
643647 "package_name" : "pkg_a" ,
644648 "rule_name" : "python_requires_check" ,
645649 "line_number" : "123" ,
646650 "matched_string" : "3.7" ,
647- "context_line" : "python_requires = '>=3.7'"
651+ "context_line" : "python_requires = '>=3.7'" ,
652+ "dependency" : "python" ,
653+ "version" : "3.7"
648654 }
649655
650656 formatted = format_for_raw_csv (match )
651657
658+ assert formatted ["file_name" ] == "setup.py"
652659 assert formatted ["file_path" ] == "google-cloud-python/main/packages/pkg_a/setup.py"
653660 assert formatted ["package_name" ] == "pkg_a"
654661 assert formatted ["rule_name" ] == "python_requires_check"
655662 assert formatted ["line_number" ] == 123 # Int conversion
656663 assert formatted ["matched_string" ] == "3.7" # No formula wrapping
657664 assert formatted ["context_line" ] == "python_requires = '>=3.7'"
665+ assert formatted ["dependency" ] == "python"
666+ assert formatted ["version" ] == "3.7"
667+
668+ def test_format_for_raw_csv_fallback_filename ():
669+ match = {
670+ "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
671+ "repo_path" : "packages/pkg_a/setup.py" ,
672+ "package_name" : "pkg_a" ,
673+ "rule_name" : "python_requires_check" ,
674+ "line_number" : "123" ,
675+ "matched_string" : "3.7" ,
676+ "context_line" : "python_requires = '>=3.7'" ,
677+ "dependency" : "python" ,
678+ "version" : "3.7"
679+ }
680+
681+ formatted = format_for_raw_csv (match )
682+ assert formatted ["file_name" ] == "setup.py"
658683
659684def test_format_for_spreadsheet ():
660685 match = {
686+ "file_name" : "setup.py" ,
661687 "file_path" : "google-cloud-python/main/packages/pkg_a/setup.py" ,
662688 "repo_path" : "packages/pkg_a/setup.py" ,
663689 "package_name" : "pkg_a" ,
664690 "rule_name" : "python_requires_check" ,
665691 "line_number" : 123 ,
666692 "matched_string" : "3.7" ,
667- "context_line" : "python_requires = '>=3.7'"
693+ "context_line" : "python_requires = '>=3.7'" ,
694+ "dependency" : "python" ,
695+ "version" : "3.7"
668696 }
669697
670698 # Without github_repo
671699 formatted_no_repo = format_for_spreadsheet (match )
700+ assert formatted_no_repo ["file_name" ] == "setup.py"
672701 assert formatted_no_repo ["line_number" ] == 123
673702 assert formatted_no_repo ["matched_string" ] == '="3.7"' # Decimal protection formula
703+ assert formatted_no_repo ["dependency" ] == "python"
704+ assert formatted_no_repo ["version" ] == "3.7"
674705
675706 # With github_repo
676707 formatted_repo = format_for_spreadsheet (match , github_repo = "https://github.com/user/repo" , branch = "main" )
@@ -695,3 +726,91 @@ def test_format_for_console():
695726 assert "3.7" in log_str
696727 assert "python_requires = " not in log_str # Slim format doesn't print context line
697728
729+ def test_parse_targets_inline_json ():
730+ from version_scanner import parse_targets
731+ json_str = '{"python": ["3.7", "3.8"], "protobuf": "4.25.8"}'
732+ targets = parse_targets (json_str )
733+ assert targets == [("python" , "3.7" ), ("python" , "3.8" ), ("protobuf" , "4.25.8" )]
734+
735+ def test_parse_targets_inline_yaml ():
736+ from version_scanner import parse_targets
737+ yaml_str = """
738+ python:
739+ - "3.7"
740+ - "3.8"
741+ protobuf: "4.25.8"
742+ """
743+ targets = parse_targets (yaml_str )
744+ assert targets == [("python" , "3.7" ), ("python" , "3.8" ), ("protobuf" , "4.25.8" )]
745+
746+ def test_parse_targets_from_file (tmp_path ):
747+ from version_scanner import parse_targets
748+ yaml_file = tmp_path / "targets.yaml"
749+ yaml_file .write_text ("""
750+ python:
751+ - "3.7"
752+ - "3.8"
753+ protobuf: "4.25.8"
754+ """ )
755+ targets = parse_targets (str (yaml_file ))
756+ assert targets == [("python" , "3.7" ), ("python" , "3.8" ), ("protobuf" , "4.25.8" )]
757+
758+ def test_parse_targets_invalid_syntax ():
759+ from version_scanner import parse_targets
760+ with pytest .raises (SystemExit ) as excinfo :
761+ parse_targets ('{"invalid"' )
762+ assert excinfo .value .code == 1
763+
764+ def test_scan_repository_multi_targets (tmp_path ):
765+ # Setup files in tmp repository
766+ file1 = tmp_path / "packages" / "pkg1" / "setup.py"
767+ file1 .parent .mkdir (parents = True )
768+ file1 .write_text ("python_requires = '>=3.7'\n " )
769+
770+ file2 = tmp_path / "packages" / "pkg2" / "requirements.txt"
771+ file2 .parent .mkdir (parents = True )
772+ file2 .write_text ("protobuf==4.25.8\n " )
773+
774+ # Let's mock a config file with rules for both python and protobuf
775+ config_file = tmp_path / "regex_config.yaml"
776+ config_file .write_text ("""
777+ rules:
778+ - name: python_requires_check
779+ applies_to:
780+ - python
781+ rules:
782+ - python_requires\\ s*=\\ s*['\" ]>={version}['\" ]
783+ - name: protobuf_check
784+ applies_to:
785+ - protobuf
786+ rules:
787+ - protobuf=={version}
788+ """ )
789+
790+ from version_scanner import ConfigManager , scan_repository
791+
792+ targets = [("python" , "3.7" ), ("protobuf" , "4.25.8" )]
793+ rules = []
794+ for dep , ver in targets :
795+ cm = ConfigManager (str (config_file ), dep , ver )
796+ rules .extend (cm .load_config ())
797+
798+ results = scan_repository (str (tmp_path ), rules , targets = targets )
799+
800+ # We should have 2 matches
801+ assert len (results ) == 2
802+
803+ # Match for python
804+ python_match = [r for r in results if r ["dependency" ] == "python" ]
805+ assert len (python_match ) == 1
806+ assert python_match [0 ]["version" ] == "3.7"
807+ assert python_match [0 ]["rule_name" ] == "python_requires_check"
808+ assert python_match [0 ]["file_name" ] == "setup.py"
809+
810+ # Match for protobuf
811+ protobuf_match = [r for r in results if r ["dependency" ] == "protobuf" ]
812+ assert len (protobuf_match ) == 1
813+ assert protobuf_match [0 ]["version" ] == "4.25.8"
814+ assert protobuf_match [0 ]["rule_name" ] == "protobuf_check"
815+ assert protobuf_match [0 ]["file_name" ] == "requirements.txt"
816+
0 commit comments