Skip to content

Commit c5f29f7

Browse files
authored
Encapsulate PackageData; use pathlib (#119)
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 4320802 commit c5f29f7

File tree

2 files changed

+74
-52
lines changed

2 files changed

+74
-52
lines changed

stub_uploader/build_wheel.py

+49-30
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
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

@@ -128,6 +128,30 @@ def __init__(self, typeshed_dir: str, distribution: str) -> None:
128128
self.stub_dir = Path(typeshed_dir) / THIRD_PARTY_NAMESPACE / distribution
129129

130130

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

199223

200-
def collect_setup_entries(base_dir: str) -> dict[str, list[str]]:
224+
def collect_package_data(base_path: Path) -> PackageData:
201225
"""Generate package data for a setuptools.setup() call.
202226
203227
This reflects the transformations done during copying in copy_stubs().
204228
"""
205229
package_data: dict[str, list[str]] = {}
206-
for entry in os.listdir(base_dir):
207-
if entry == META:
230+
for entry in base_path.iterdir():
231+
if entry.name == META:
208232
# Metadata file entry is added at the end.
209233
continue
210-
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")):
234+
if entry.is_file():
235+
if entry.suffix != ".pyi":
236+
if entry.suffix not in (".md", ".rst"):
214237
if (
215238
subprocess.run(
216-
["git", "check-ignore", entry], cwd=base_dir
239+
["git", "check-ignore", entry.name], cwd=str(base_path)
217240
).returncode
218241
!= 0
219242
):
220-
raise ValueError(f"Only stub files are allowed, not {entry!r}")
243+
raise ValueError(
244+
f"Only stub files are allowed, not {entry.name!r}"
245+
)
221246
continue
222-
entry = entry.split(".")[0] + SUFFIX
247+
pkg_name = entry.stem + SUFFIX
223248
# Module -> package transformation is done while copying.
224-
package_data[entry] = ["__init__.pyi"]
249+
package_data[pkg_name] = ["__init__.pyi"]
225250
else:
226-
if entry == TESTS_NAMESPACE:
251+
if entry.name == TESTS_NAMESPACE:
227252
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
253+
pkg_name = entry.name + SUFFIX
254+
package_data[pkg_name] = find_stub_files(str(entry))
255+
package_data[pkg_name].append(META)
256+
return PackageData(base_path, package_data)
234257

235258

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")
259+
def add_partial_markers(pkg_data: PackageData) -> None:
260+
for package in pkg_data.top_level_packages:
261+
pkg_data.add_file(package, "py.typed", "partial\n")
243262

244263

245264
def generate_setup_file(
@@ -253,9 +272,9 @@ def generate_setup_file(
253272
all_requirements = [
254273
str(req) for req in metadata.requires_typeshed + metadata.requires_external
255274
]
256-
package_data = collect_setup_entries(str(build_data.stub_dir))
275+
pkg_data = collect_package_data(build_data.stub_dir)
257276
if metadata.partial:
258-
add_partial_marker(package_data, str(build_data.stub_dir))
277+
add_partial_markers(pkg_data)
259278
requires_python = (
260279
metadata.requires_python
261280
if metadata.requires_python is not None
@@ -269,8 +288,8 @@ def generate_setup_file(
269288
),
270289
version=version,
271290
requires=all_requirements,
272-
packages=list(package_data.keys()),
273-
package_data=package_data,
291+
packages=pkg_data.top_level_packages,
292+
package_data=pkg_data.package_data,
274293
requires_python=requires_python,
275294
)
276295

tests/test_unit.py

+25-22
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)