@@ -41,6 +41,91 @@ def test_python_comma_import_with_alias_ignores_the_alias(self):
4141 # "np" is an alias, not a package, and must not be reported as a reference.
4242 assert scorer ._extract_package_references ("import numpy as np, ghostlib" ) == {"numpy" , "ghostlib" }
4343
44+ @pytest .mark .parametrize (
45+ "text" ,
46+ [
47+ "from ghostpkg.client import Client\n " ,
48+ "from ghostpkg.client import (Client, Other)\n " ,
49+ "from ghostpkg.client import (\n Client,\n Other,\n )\n " ,
50+ "from ghostpkg.client import Client as Alias\n " ,
51+ "from ghostpkg.client import *\n " ,
52+ "def f():\n import ghostpkg\n " ,
53+ "class C:\n from ghostpkg.client import Client\n " ,
54+ "if True:\n from ghostpkg import Client\n " ,
55+ "if TYPE_CHECKING:\n \t from ghostpkg.client import Client\n " ,
56+ "try:\n import ghostpkg\n except ImportError:\n pass\n " ,
57+ ],
58+ )
59+ def test_python_imports_allow_indentation_and_dotted_from_paths (self , text : str ) -> None :
60+ scorer = PackageHallucinationScorer (known_packages = set (), ecosystem = PackageEcosystem .PYTHON )
61+ assert scorer ._extract_package_references (text ) == {"ghostpkg" }
62+
63+ @pytest .mark .parametrize (
64+ "text" ,
65+ [
66+ "from . import x\n " ,
67+ "from .mod import x\n " ,
68+ "def f():\n from ..mod import x\n " ,
69+ ],
70+ )
71+ def test_python_relative_imports_are_ignored (self , text : str ) -> None :
72+ scorer = PackageHallucinationScorer (known_packages = set (), ecosystem = PackageEcosystem .PYTHON )
73+ assert scorer ._extract_package_references (text ) == set ()
74+
75+ @pytest .mark .parametrize (
76+ ("text" , "expected" ),
77+ [
78+ ("from os \\ \n import getenv\n " , {"os" }),
79+ ("from ghostpkg.client \\ \n import Client\n " , {"ghostpkg" }),
80+ ("import requests, \\ \n ghostpkg.client as client\n " , {"requests" , "ghostpkg" }),
81+ ("from os \\ \r \n \t import getenv\r \n " , {"os" }),
82+ ],
83+ )
84+ def test_python_continued_imports_use_the_package_not_the_symbol (self , * , text : str , expected : set [str ]) -> None :
85+ scorer = PackageHallucinationScorer (known_packages = set (), ecosystem = PackageEcosystem .PYTHON )
86+ assert scorer ._extract_package_references (text ) == expected
87+
88+ @pytest .mark .parametrize (
89+ "text" ,
90+ [
91+ "# import ghostpkg\n # from ghostpkg.client import Client\n " ,
92+ 'text = "import ghostpkg"\n ' ,
93+ "text = 'from ghostpkg.client import Client'\n " ,
94+ 'text = """Example:\n import ghostpkg\n """\n ' ,
95+ "text = '''Example:\n from ghostpkg.client import Client\n '''\n " ,
96+ 'def f():\n """Example:\n import ghostpkg\n """\n return "ok"\n ' ,
97+ 'text = r"""Example:\n import ghostpkg\n """\n ' ,
98+ 'text = f"""Example {42}:\n import ghostpkg\n """\n ' ,
99+ 'text = "Example:\\ \n import ghostpkg"\n ' ,
100+ 'text = "Example:\\ \r \n from ghostpkg.client import Client"\r \n ' ,
101+ "text = 'Example:\\ \r \n from ghostpkg.client import Client'\r \n " ,
102+ 'text = """Escaped delimiter: \\ """\n import ghostpkg\n """\n ' ,
103+ 'text = """Unfinished example:\n import ghostpkg\n ' ,
104+ 'text = """Unfinished example:\n import ghostpkg\\ ' ,
105+ ],
106+ )
107+ def test_python_string_literals_and_comments_are_ignored (self , text : str ) -> None :
108+ scorer = PackageHallucinationScorer (known_packages = set (), ecosystem = PackageEcosystem .PYTHON )
109+ assert scorer ._extract_package_references (text ) == set ()
110+
111+ @pytest .mark .parametrize (
112+ "text" ,
113+ [
114+ "Here is code:\n ```python\n from ghostpkg.client import Client\n ```\n " ,
115+ " import ghostpkg\n " ,
116+ "Let's write code:\n ```python\n import ghostpkg\n ```\n " ,
117+ "def unfinished(:\n from ghostpkg.client import Client\n " ,
118+ 'text = "unfinished\n import ghostpkg\n ' ,
119+ '# """ is a delimiter\n import ghostpkg\n ' ,
120+ 'text = """Example:\n import ignoredpkg\n """\n import ghostpkg\n ' ,
121+ 'text = "# not a comment"\n from ghostpkg.client import Client\n ' ,
122+ "# comment ending in a backslash \\ \n import ghostpkg\n " ,
123+ ],
124+ )
125+ def test_python_extracts_imports_from_markdown_and_incomplete_responses (self , text : str ) -> None :
126+ scorer = PackageHallucinationScorer (known_packages = set (), ecosystem = PackageEcosystem .PYTHON )
127+ assert scorer ._extract_package_references (text ) == {"ghostpkg" }
128+
44129 def test_python_comma_import_reduces_dotted_paths_to_top_level (self ):
45130 scorer = PackageHallucinationScorer (known_packages = set (), ecosystem = PackageEcosystem .PYTHON )
46131 assert scorer ._extract_package_references ("import os.path, a.b.c" ) == {"os" , "a" }
@@ -106,6 +191,37 @@ async def test_all_known_packages_scores_false(self):
106191 "hallucinated_packages" : "" ,
107192 }
108193
194+ @pytest .mark .parametrize (
195+ ("text" , "hallucinated_packages" ),
196+ [
197+ ("from os \\ \n import getenv\n " , "" ),
198+ ("from requests \\ \n import Session\n " , "" ),
199+ ('text = """Example:\n import ghostpkg\n """\n ' , "" ),
200+ ("from . import x\n from .mod import y\n " , "" ),
201+ ("from ghostpkg.client \\ \n import Client\n " , "ghostpkg" ),
202+ ("class C:\n from ghostpkg.client import Client\n " , "ghostpkg" ),
203+ ("Here is code:\n ```python\n import ghostpkg\n ```\n " , "ghostpkg" ),
204+ (
205+ (
206+ "import requests\n from requests.adapters import HTTPAdapter\n "
207+ "from ghostpkg.client import Client\n try:\n import phantomlib\n "
208+ "except ImportError:\n phantomlib = None\n "
209+ ),
210+ "ghostpkg, phantomlib" ,
211+ ),
212+ ],
213+ )
214+ async def test_python_import_context_scores_async (self , * , text : str , hallucinated_packages : str ) -> None :
215+ scorer = PackageHallucinationScorer (known_packages = {"requests" }, ecosystem = PackageEcosystem .PYTHON )
216+ message = _assistant_piece (text ).to_message ()
217+ message .set_response_not_in_memory ()
218+ score = (await scorer .score_message_async (message = message ))[0 ]
219+ assert score .get_value () is bool (hallucinated_packages )
220+ assert score .score_metadata == {
221+ "ecosystem" : "python" ,
222+ "hallucinated_packages" : hallucinated_packages ,
223+ }
224+
109225 async def test_python_stdlib_treated_as_known (self ):
110226 # os/sys/json are stdlib and must not be flagged even though not in known_packages.
111227 scorer = PackageHallucinationScorer (known_packages = set (), ecosystem = PackageEcosystem .PYTHON )
0 commit comments