Skip to content

Commit 084a94a

Browse files
authored
dependency type stubs (#651)
* add 2 type stubs * add typing to poetry * fix test * add another unit test
1 parent 2882bfc commit 084a94a

File tree

4 files changed

+304
-18
lines changed

4 files changed

+304
-18
lines changed

src/codemodder/dependency.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class Dependency:
1717
oss_link: str
1818
package_link: str
1919
hashes: list[str] = field(default_factory=list)
20+
# Forward reference
21+
type_stubs: list["Dependency"] = field(default_factory=list)
2022

2123
@property
2224
def name(self) -> str:
@@ -56,6 +58,24 @@ def __hash__(self):
5658
),
5759
oss_link="https://github.com/wtforms/flask-wtf/",
5860
package_link="https://pypi.org/project/Flask-WTF/",
61+
type_stubs=[
62+
Dependency(
63+
Requirement("types-WTForms==3.1.0.20240425"),
64+
hashes=[
65+
"449b6e3756b2bc70657e98d989bdbf572a25466428774be96facf9debcbf6c4e",
66+
"49ffc1fe5576ea0735b763fff77e7060dd39ecc661276cbd0b47099921b3a6f2",
67+
],
68+
description="""\
69+
This is a type stub package for the WTForms package.
70+
""",
71+
_license=License(
72+
"Apache-2.0",
73+
"https://opensource.org/license/apache-2-0",
74+
),
75+
oss_link="https://github.com/python/typeshed",
76+
package_link="https://pypi.org/project/types-WTForms/",
77+
),
78+
],
5979
)
6080

6181
DefusedXML = Dependency(
@@ -74,6 +94,24 @@ def __hash__(self):
7494
),
7595
oss_link="https://github.com/tiran/defusedxml",
7696
package_link="https://pypi.org/project/defusedxml/",
97+
type_stubs=[
98+
Dependency(
99+
Requirement("types-defusedxml==0.7.0.20240218"),
100+
hashes=[
101+
"2b7f3c5ca14fdbe728fab0b846f5f7eb98c4bd4fd2b83d25f79e923caa790ced",
102+
"05688a7724dc66ea74c4af5ca0efc554a150c329cb28c13a64902cab878d06ed",
103+
],
104+
description="""\
105+
This is a type stub package for the defusedxml package.
106+
""",
107+
_license=License(
108+
"Apache-2.0",
109+
"https://opensource.org/license/apache-2-0",
110+
),
111+
oss_link="https://github.com/python/typeshed",
112+
package_link="https://pypi.org/project/types-defusedxml/",
113+
),
114+
],
77115
)
78116

79117
Security = Dependency(

src/codemodder/dependency_management/codemod_dependencies.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# dependency in dependency.py. Be sure to update the version AND the hashes.
66
# Run `get-hashes pkg==version` to get the hashes.
77
defusedxml==0.7.1
8+
types-defusedxml==0.7.0.20240218
89
flask-wtf==1.2.0
10+
types-WTForms==3.1.0.20240425
911
security==1.2.1
1012
fickling==0.1.3

src/codemodder/dependency_management/pyproject_writer.py

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from codemodder.diff import create_diff_and_linenums
1010
from codemodder.logging import logger
1111

12+
TYPE_CHECKER_LIBRARIES = ["mypy", "pyright"]
13+
1214

1315
def added_line_nums_strategy(lines, i):
1416
return lines[i]
@@ -21,26 +23,11 @@ def add_to_file(
2123
pyproject = self._parse_file()
2224
original = deepcopy(pyproject)
2325

24-
if poetry_data := pyproject.get("tool", {}).get("poetry", {}):
25-
add_newline = False
26+
if pyproject.get("tool", {}).get("poetry", {}):
2627
# It's unlikely and bad practice to declare dependencies under [project].dependencies
2728
# and [tool.poetry.dependencies] but if it happens, we will give priority to poetry
2829
# and add dependencies under its system.
29-
if poetry_data.get("dependencies") is None:
30-
pyproject["tool"]["poetry"].append("dependencies", {})
31-
add_newline = True
32-
33-
for dep in dependencies:
34-
try:
35-
pyproject["tool"]["poetry"]["dependencies"].append(
36-
dep.requirement.name, str(dep.requirement.specifier)
37-
)
38-
except tomlkit.exceptions.KeyAlreadyPresent:
39-
pass
40-
41-
if add_newline:
42-
pyproject["tool"]["poetry"]["dependencies"].add(tomlkit.nl())
43-
30+
self._update_poetry(pyproject, dependencies)
4431
else:
4532
try:
4633
pyproject["project"]["dependencies"].extend(
@@ -70,3 +57,67 @@ def add_to_file(
7057
def _parse_file(self):
7158
with open(self.path, encoding="utf-8") as f:
7259
return tomlkit.load(f)
60+
61+
def _update_poetry(
62+
self,
63+
pyproject: tomlkit.toml_document.TOMLDocument,
64+
dependencies: list[Dependency],
65+
):
66+
add_newline = False
67+
68+
if pyproject.get("tool", {}).get("poetry", {}).get("dependencies") is None:
69+
pyproject["tool"]["poetry"].update({"dependencies": {}})
70+
add_newline = True
71+
72+
typing_location = find_typing_location(pyproject)
73+
74+
for dep in dependencies:
75+
try:
76+
pyproject["tool"]["poetry"]["dependencies"].append(
77+
dep.requirement.name, str(dep.requirement.specifier)
78+
)
79+
except tomlkit.exceptions.KeyAlreadyPresent:
80+
pass
81+
82+
for type_stub_dependency in dep.type_stubs:
83+
if typing_location:
84+
try:
85+
keys = typing_location.split(".")
86+
section = pyproject["tool"]["poetry"]
87+
for key in keys:
88+
section = section[key]
89+
section.append(
90+
type_stub_dependency.requirement.name,
91+
str(type_stub_dependency.requirement.specifier),
92+
)
93+
except tomlkit.exceptions.KeyAlreadyPresent:
94+
pass
95+
96+
if add_newline:
97+
pyproject["tool"]["poetry"]["dependencies"].add(tomlkit.nl())
98+
99+
100+
def find_typing_location(pyproject):
101+
"""
102+
Look for a typing tool declared as a dependency in project.toml
103+
"""
104+
locations = [
105+
"dependencies",
106+
"test.dependencies",
107+
"dev-dependencies",
108+
"dev.dependencies",
109+
"group.test.dependencies",
110+
]
111+
poetry_section = pyproject.get("tool", {}).get("poetry", {})
112+
113+
for location in locations:
114+
keys = location.split(".")
115+
section = poetry_section
116+
try:
117+
for key in keys:
118+
section = section[key]
119+
if any(checker in section for checker in TYPE_CHECKER_LIBRARIES):
120+
return location
121+
except KeyError:
122+
continue
123+
return None

tests/dependency_management/test_pyproject_writer.py

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
from codemodder.codetf import DiffSide
66
from codemodder.dependency import DefusedXML, Security
7-
from codemodder.dependency_management.pyproject_writer import PyprojectWriter
7+
from codemodder.dependency_management.pyproject_writer import (
8+
TYPE_CHECKER_LIBRARIES,
9+
PyprojectWriter,
10+
)
811
from codemodder.project_analysis.file_parsers.package_store import (
912
FileType,
1013
PackageStore,
@@ -408,3 +411,195 @@ def test_pyproject_poetry_no_declared_deps(tmpdir):
408411
"""
409412

410413
assert pyproject_toml.read() == dedent(updated_pyproject)
414+
415+
416+
@pytest.mark.parametrize("type_checker", TYPE_CHECKER_LIBRARIES)
417+
def test_pyproject_poetry_with_type_checker_tool(tmpdir, type_checker):
418+
orig_pyproject = f"""\
419+
[tool.poetry]
420+
name = "example-project"
421+
version = "0.1.0"
422+
description = "An example project to demonstrate Poetry configuration."
423+
authors = ["Your Name <[email protected]>"]
424+
425+
[build-system]
426+
requires = ["poetry-core>=1.0.0"]
427+
build-backend = "poetry.core.masonry.api"
428+
429+
[tool.poetry.dependencies]
430+
python = "~=3.11.0"
431+
requests = ">=2.25.1,<3.0.0"
432+
pandas = "^1.2.3"
433+
libcst = ">1.0"
434+
{type_checker} = "==1.0"
435+
"""
436+
437+
pyproject_toml = tmpdir.join("pyproject.toml")
438+
pyproject_toml.write(dedent(orig_pyproject))
439+
440+
store = PackageStore(
441+
type=FileType.TOML,
442+
file=pyproject_toml,
443+
dependencies=set(),
444+
py_versions=["~=3.11.0"],
445+
)
446+
447+
writer = PyprojectWriter(store, tmpdir)
448+
dependencies = [Security, DefusedXML]
449+
writer.write(dependencies)
450+
451+
defusedxml_type_stub = DefusedXML.type_stubs[0]
452+
updated_pyproject = f"""\
453+
[tool.poetry]
454+
name = "example-project"
455+
version = "0.1.0"
456+
description = "An example project to demonstrate Poetry configuration."
457+
authors = ["Your Name <[email protected]>"]
458+
459+
[build-system]
460+
requires = ["poetry-core>=1.0.0"]
461+
build-backend = "poetry.core.masonry.api"
462+
463+
[tool.poetry.dependencies]
464+
python = "~=3.11.0"
465+
requests = ">=2.25.1,<3.0.0"
466+
pandas = "^1.2.3"
467+
libcst = ">1.0"
468+
{type_checker} = "==1.0"
469+
{Security.requirement.name} = "{str(Security.requirement.specifier)}"
470+
{DefusedXML.requirement.name} = "{str(DefusedXML.requirement.specifier)}"
471+
{defusedxml_type_stub.requirement.name} = "{str(defusedxml_type_stub.requirement.specifier)}"
472+
"""
473+
474+
assert pyproject_toml.read() == dedent(updated_pyproject)
475+
476+
477+
@pytest.mark.parametrize(
478+
"dependency_section",
479+
[
480+
"[tool.poetry.test.dependencies]",
481+
"[tool.poetry.dev-dependencies]",
482+
"[tool.poetry.dev.dependencies]",
483+
"[tool.poetry.group.test.dependencies]",
484+
],
485+
)
486+
@pytest.mark.parametrize("type_checker", TYPE_CHECKER_LIBRARIES)
487+
def test_pyproject_poetry_with_type_checker_tool_without_poetry_deps_section(
488+
tmpdir, type_checker, dependency_section
489+
):
490+
orig_pyproject = f"""\
491+
[tool.poetry]
492+
name = "example-project"
493+
version = "0.1.0"
494+
description = "An example project to demonstrate Poetry configuration."
495+
authors = ["Your Name <[email protected]>"]
496+
497+
[build-system]
498+
requires = ["poetry-core>=1.0.0"]
499+
build-backend = "poetry.core.masonry.api"
500+
501+
{dependency_section}
502+
{type_checker} = "==1.0"
503+
"""
504+
505+
pyproject_toml = tmpdir.join("pyproject.toml")
506+
pyproject_toml.write(dedent(orig_pyproject))
507+
508+
store = PackageStore(
509+
type=FileType.TOML,
510+
file=pyproject_toml,
511+
dependencies=set(),
512+
py_versions=["~=3.11.0"],
513+
)
514+
515+
writer = PyprojectWriter(store, tmpdir)
516+
dependencies = [Security, DefusedXML]
517+
writer.write(dependencies)
518+
519+
defusedxml_type_stub = DefusedXML.type_stubs[0]
520+
updated_pyproject = f"""\
521+
[tool.poetry]
522+
name = "example-project"
523+
version = "0.1.0"
524+
description = "An example project to demonstrate Poetry configuration."
525+
authors = ["Your Name <[email protected]>"]
526+
527+
[tool.poetry.dependencies]
528+
{Security.requirement.name} = "{str(Security.requirement.specifier)}"
529+
{DefusedXML.requirement.name} = "{str(DefusedXML.requirement.specifier)}"
530+
531+
[build-system]
532+
requires = ["poetry-core>=1.0.0"]
533+
build-backend = "poetry.core.masonry.api"
534+
535+
{dependency_section}
536+
{type_checker} = "==1.0"
537+
{defusedxml_type_stub.requirement.name} = "{str(defusedxml_type_stub.requirement.specifier)}"
538+
"""
539+
540+
assert pyproject_toml.read() == dedent(updated_pyproject)
541+
542+
543+
@pytest.mark.parametrize("type_checker", TYPE_CHECKER_LIBRARIES)
544+
def test_pyproject_poetry_with_type_checker_tool_multiple(tmpdir, type_checker):
545+
orig_pyproject = f"""\
546+
[build-system]
547+
requires = ["poetry-core>=1.0.0"]
548+
build-backend = "poetry.core.masonry.api"
549+
550+
[tool.poetry]
551+
name = "example-project"
552+
version = "0.1.0"
553+
description = "An example project to demonstrate Poetry configuration."
554+
authors = ["Your Name <[email protected]>"]
555+
556+
[tool.poetry.dependencies]
557+
python = "~=3.11.0"
558+
requests = ">=2.25.1,<3.0.0"
559+
pandas = "^1.2.3"
560+
libcst = ">1.0"
561+
562+
[tool.poetry.group.test.dependencies]
563+
{type_checker} = "==1.0"
564+
"""
565+
566+
pyproject_toml = tmpdir.join("pyproject.toml")
567+
pyproject_toml.write(dedent(orig_pyproject))
568+
569+
store = PackageStore(
570+
type=FileType.TOML,
571+
file=pyproject_toml,
572+
dependencies=set(),
573+
py_versions=["~=3.11.0"],
574+
)
575+
576+
writer = PyprojectWriter(store, tmpdir)
577+
dependencies = [Security, DefusedXML]
578+
writer.write(dependencies)
579+
580+
defusedxml_type_stub = DefusedXML.type_stubs[0]
581+
updated_pyproject = f"""\
582+
[build-system]
583+
requires = ["poetry-core>=1.0.0"]
584+
build-backend = "poetry.core.masonry.api"
585+
586+
[tool.poetry]
587+
name = "example-project"
588+
version = "0.1.0"
589+
description = "An example project to demonstrate Poetry configuration."
590+
authors = ["Your Name <[email protected]>"]
591+
592+
[tool.poetry.dependencies]
593+
python = "~=3.11.0"
594+
requests = ">=2.25.1,<3.0.0"
595+
pandas = "^1.2.3"
596+
libcst = ">1.0"
597+
{Security.requirement.name} = "{str(Security.requirement.specifier)}"
598+
{DefusedXML.requirement.name} = "{str(DefusedXML.requirement.specifier)}"
599+
600+
[tool.poetry.group.test.dependencies]
601+
{type_checker} = "==1.0"
602+
{defusedxml_type_stub.requirement.name} = "{str(defusedxml_type_stub.requirement.specifier)}"
603+
"""
604+
605+
assert pyproject_toml.read() == dedent(updated_pyproject)

0 commit comments

Comments
 (0)