Skip to content

Commit 7c6a5d1

Browse files
committed
Encapsulate PackageData; use pathlib
This is a preliminary refactoring for fixing python/typeshed#11254. The idea is that the new class `PackageData` encapsulates all data concerning packages and their contents. This allows us later to find the top-level non-namespace packages, instead of just the top-level packages as we are doing now.
1 parent ae7ebd5 commit 7c6a5d1

File tree

2 files changed

+76
-51
lines changed

2 files changed

+76
-51
lines changed

stub_uploader/build_wheel.py

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@
2323
import argparse
2424
import os
2525
import os.path
26-
from pathlib import Path
2726
import shutil
2827
import subprocess
2928
import sys
3029
import tempfile
30+
from pathlib import Path
3131
from textwrap import dedent
3232
from typing import Optional
3333

34+
from genericpath import isfile
35+
3436
from stub_uploader.const import (
3537
CHANGELOG_PATH,
3638
META,
@@ -128,6 +130,30 @@ def __init__(self, typeshed_dir: str, distribution: str) -> None:
128130
self.stub_dir = Path(typeshed_dir) / THIRD_PARTY_NAMESPACE / distribution
129131

130132

133+
class PackageData:
134+
"""Information about the packages of a distribution and their contents."""
135+
136+
def __init__(self, base_path: Path, package_data: dict[str, list[str]]) -> None:
137+
self.base_path = base_path
138+
self.package_data = package_data
139+
140+
@property
141+
def top_level_packages(self) -> list[str]:
142+
"""Top level package names.
143+
144+
These are the packages that are not subpackages of any other package
145+
and includes namespace packages.
146+
"""
147+
return list(self.package_data.keys())
148+
149+
def add_file(self, package: str, filename: str, file_contents: str) -> None:
150+
"""Add a file to a package."""
151+
entry_path = self.base_path / package
152+
entry_path.mkdir(exist_ok=True)
153+
(entry_path / filename).write_text(file_contents)
154+
self.package_data[package].append(filename)
155+
156+
131157
def find_stub_files(top: str) -> list[str]:
132158
"""Find all stub files for a given package, relative to package root.
133159
@@ -197,49 +223,45 @@ def copy_changelog(distribution: str, dst: str) -> None:
197223
pass # Ignore missing changelogs
198224

199225

200-
def collect_setup_entries(base_dir: str) -> dict[str, list[str]]:
226+
def collect_package_data(base_path: Path) -> PackageData:
201227
"""Generate package data for a setuptools.setup() call.
202228
203229
This reflects the transformations done during copying in copy_stubs().
204230
"""
205231
package_data: dict[str, list[str]] = {}
206-
for entry in os.listdir(base_dir):
207-
if entry == META:
232+
for entry in base_path.iterdir():
233+
if entry.name == META:
208234
# Metadata file entry is added at the end.
209235
continue
210236
original_entry = entry
211-
if os.path.isfile(os.path.join(base_dir, entry)):
212-
if not entry.endswith(".pyi"):
213-
if not entry.endswith((".md", ".rst")):
237+
if entry.is_file():
238+
if entry.suffix != ".pyi":
239+
if entry.suffix not in (".md", ".rst"):
214240
if (
215241
subprocess.run(
216-
["git", "check-ignore", entry], cwd=base_dir
242+
["git", "check-ignore", entry.name], cwd=str(base_path)
217243
).returncode
218244
!= 0
219245
):
220-
raise ValueError(f"Only stub files are allowed, not {entry!r}")
246+
raise ValueError(
247+
f"Only stub files are allowed, not {entry.name!r}"
248+
)
221249
continue
222-
entry = entry.split(".")[0] + SUFFIX
250+
pkg_name = entry.name.split(".")[0] + SUFFIX
223251
# Module -> package transformation is done while copying.
224-
package_data[entry] = ["__init__.pyi"]
252+
package_data[pkg_name] = ["__init__.pyi"]
225253
else:
226-
if entry == TESTS_NAMESPACE:
254+
if entry.name == TESTS_NAMESPACE:
227255
continue
228-
entry += SUFFIX
229-
package_data[entry] = find_stub_files(
230-
os.path.join(base_dir, original_entry)
231-
)
232-
package_data[entry].append(META)
233-
return package_data
256+
pkg_name = entry.name + SUFFIX
257+
package_data[pkg_name] = find_stub_files(str(original_entry))
258+
package_data[pkg_name].append(META)
259+
return PackageData(base_path, package_data)
234260

235261

236-
def add_partial_marker(package_data: dict[str, list[str]], stub_dir: str) -> None:
237-
for entry, files in package_data.items():
238-
entry_path = os.path.join(stub_dir, entry)
239-
os.makedirs(entry_path, exist_ok=True)
240-
with open(os.path.join(entry_path, "py.typed"), "w") as py_typed:
241-
py_typed.write("partial\n")
242-
files.append("py.typed")
262+
def add_partial_markers(pkg_data: PackageData) -> None:
263+
for package in pkg_data.top_level_packages:
264+
pkg_data.add_file(package, "py.typed", "partial\n")
243265

244266

245267
def generate_setup_file(
@@ -253,9 +275,9 @@ def generate_setup_file(
253275
all_requirements = [
254276
str(req) for req in metadata.requires_typeshed + metadata.requires_external
255277
]
256-
package_data = collect_setup_entries(str(build_data.stub_dir))
278+
pkg_data = collect_package_data(build_data.stub_dir)
257279
if metadata.partial:
258-
add_partial_marker(package_data, str(build_data.stub_dir))
280+
add_partial_markers(pkg_data)
259281
requires_python = (
260282
metadata.requires_python
261283
if metadata.requires_python is not None
@@ -269,8 +291,8 @@ def generate_setup_file(
269291
),
270292
version=version,
271293
requires=all_requirements,
272-
packages=list(package_data.keys()),
273-
package_data=package_data,
294+
packages=pkg_data.top_level_packages,
295+
package_data=pkg_data.package_data,
274296
requires_python=requires_python,
275297
)
276298

tests/test_unit.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
"""Unit tests for simple helpers should go here."""
22

33
import datetime
4-
from io import StringIO
54
import os
65
import tempfile
6+
from io import StringIO
7+
from pathlib import Path
78
from typing import Any
89

910
import pytest
1011
from packaging.version import Version
1112

12-
from stub_uploader.build_wheel import collect_setup_entries
13-
from stub_uploader.get_version import (
14-
compute_stub_version,
15-
ensure_specificity,
16-
)
17-
from stub_uploader.metadata import _UploadedPackages, strip_types_prefix, Metadata
13+
from stub_uploader.build_wheel import collect_package_data
14+
from stub_uploader.get_version import compute_stub_version, ensure_specificity
15+
from stub_uploader.metadata import Metadata, _UploadedPackages, strip_types_prefix
1816
from stub_uploader.ts_data import parse_requirements
1917

2018

@@ -99,13 +97,17 @@ def test_compute_stub_version() -> None:
9997
)
10098

10199

102-
def test_collect_setup_entries() -> None:
103-
stubs = os.path.join("data", "test_typeshed", "stubs")
104-
entries = collect_setup_entries(os.path.join(stubs, "singlefilepkg"))
105-
assert entries == ({"singlefilepkg-stubs": ["__init__.pyi", "METADATA.toml"]})
100+
def test_collect_package_data() -> None:
101+
stubs = Path("data") / "test_typeshed" / "stubs"
102+
pkg_data = collect_package_data(stubs / "singlefilepkg")
103+
assert pkg_data.top_level_packages == ["singlefilepkg-stubs"]
104+
assert pkg_data.package_data == (
105+
{"singlefilepkg-stubs": ["__init__.pyi", "METADATA.toml"]}
106+
)
106107

107-
entries = collect_setup_entries(os.path.join(stubs, "multifilepkg"))
108-
assert entries == (
108+
pkg_data = collect_package_data(stubs / "multifilepkg")
109+
assert pkg_data.top_level_packages == ["multifilepkg-stubs"]
110+
assert pkg_data.package_data == (
109111
{
110112
"multifilepkg-stubs": [
111113
"__init__.pyi",
@@ -119,8 +121,9 @@ def test_collect_setup_entries() -> None:
119121
}
120122
)
121123

122-
entries = collect_setup_entries(os.path.join(stubs, "nspkg"))
123-
assert entries == (
124+
pkg_data = collect_package_data(stubs / "nspkg")
125+
assert pkg_data.top_level_packages == ["nspkg-stubs"]
126+
assert pkg_data.package_data == (
124127
{
125128
"nspkg-stubs": [
126129
os.path.join("innerpkg", "__init__.pyi"),
@@ -130,25 +133,25 @@ def test_collect_setup_entries() -> None:
130133
)
131134

132135

133-
def test_collect_setup_entries_bogusfile() -> None:
134-
stubs = os.path.join("data", "test_typeshed", "stubs")
136+
def test_collect_package_data_bogusfile() -> None:
137+
stubs = Path("data") / "test_typeshed" / "stubs"
135138
with pytest.raises(
136139
ValueError, match="Only stub files are allowed, not 'bogusfile.txt'"
137140
):
138-
collect_setup_entries(os.path.join(stubs, "bogusfiles"))
141+
collect_package_data(stubs / "bogusfiles")
139142

140143
# Make sure gitignored files aren't collected, nor do they crash function
141144
with open(os.path.join(stubs, "singlefilepkg", ".METADATA.toml.swp"), "w"):
142145
pass
143-
entries = collect_setup_entries(os.path.join(stubs, "singlefilepkg"))
144-
assert len(entries["singlefilepkg-stubs"]) == 2
146+
pkg_data = collect_package_data(stubs / "singlefilepkg")
147+
assert len(pkg_data.package_data["singlefilepkg-stubs"]) == 2
145148

146149
with open(
147150
os.path.join(stubs, "multifilepkg", "multifilepkg", ".METADATA.toml.swp"), "w"
148151
):
149152
pass
150-
entries = collect_setup_entries(os.path.join(stubs, "multifilepkg"))
151-
assert len(entries["multifilepkg-stubs"]) == 7
153+
pkg_data = collect_package_data(stubs / "multifilepkg")
154+
assert len(pkg_data.package_data["multifilepkg-stubs"]) == 7
152155

153156

154157
def test_uploaded_packages() -> None:

0 commit comments

Comments
 (0)