Skip to content

Commit 31c2773

Browse files
authored
Merge pull request #3 from python-project-templates/tkp/t
More reasonable defaults
2 parents 1b60d09 + 0848601 commit 31c2773

File tree

3 files changed

+430
-44
lines changed

3 files changed

+430
-44
lines changed

check_dist/_cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ def main(argv: list[str] | None = None) -> None:
3434
"--pre-built",
3535
metavar="DIR",
3636
default=None,
37-
help="Skip building and use existing dist files from DIR",
37+
help="Use existing dist files from DIR instead of building",
38+
)
39+
parser.add_argument(
40+
"--rebuild",
41+
action="store_true",
42+
help="Force a fresh build even when pre-built dists exist in dist/ or wheelhouse/",
3843
)
3944
args = parser.parse_args(argv)
4045

@@ -44,6 +49,7 @@ def main(argv: list[str] | None = None) -> None:
4449
no_isolation=args.no_isolation,
4550
verbose=args.verbose,
4651
pre_built=args.pre_built,
52+
rebuild=args.rebuild,
4753
)
4854
for msg in messages:
4955
print(msg)

check_dist/_core.py

Lines changed: 139 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ def load_config(pyproject_path: str | Path = "pyproject.toml", *, source_dir: st
9999
if source_dir is None:
100100
source_dir = path.parent
101101
copier_cfg = load_copier_config(source_dir)
102-
defaults = copier_defaults(copier_cfg)
102+
hatch_cfg = config.get("tool", {}).get("hatch", {}).get("build", {})
103+
defaults = copier_defaults(copier_cfg, hatch_config=hatch_cfg)
103104
if defaults is not None:
104105
return defaults
105106
return empty
@@ -145,7 +146,7 @@ def load_hatch_config(pyproject_path: str | Path = "pyproject.toml") -> dict:
145146
},
146147
"rust": {
147148
"sdist_present_extra": ["rust", "src", "Cargo.toml", "Cargo.lock"],
148-
"sdist_absent_extra": [".gitattributes", "target"],
149+
"sdist_absent_extra": [".gitattributes", ".vscode", "target"],
149150
"wheel_absent_extra": ["rust", "src", "Cargo.toml"],
150151
},
151152
"js": {
@@ -216,12 +217,18 @@ def _module_name_from_project(project_name: str) -> str:
216217
return re.sub(r"[\s-]+", "_", project_name).strip("_")
217218

218219

219-
def copier_defaults(copier_config: dict) -> dict | None:
220+
def copier_defaults(copier_config: dict, hatch_config: dict | None = None) -> dict | None:
220221
"""Derive default check-dist config from copier answers.
221222
222223
Returns a config dict with the same shape as ``load_config`` output,
223224
or ``None`` if deriving defaults is not possible (no ``add_extension``
224225
key, or unknown extension type).
226+
227+
When *hatch_config* is provided, ``sdist_present_extra`` patterns are
228+
filtered against the hatch sdist configuration to only require paths
229+
that will actually appear in the archive. Follows hatch precedence:
230+
``only-include`` (exhaustive) > ``packages`` (used as ``only-include``
231+
fallback) > ``include`` (filter on full tree).
225232
"""
226233
extension = copier_config.get("add_extension")
227234
project_name = copier_config.get("project_name")
@@ -234,7 +241,12 @@ def copier_defaults(copier_config: dict) -> dict | None:
234241

235242
module = _module_name_from_project(project_name)
236243

237-
sdist_present = [module, *ext_defaults.get("sdist_present_extra", []), *_COMMON_SDIST_PRESENT]
244+
sdist_present_extra = list(ext_defaults.get("sdist_present_extra", []))
245+
246+
if hatch_config:
247+
sdist_present_extra = _filter_extras_by_hatch(sdist_present_extra, hatch_config)
248+
249+
sdist_present = [module, *sdist_present_extra, *_COMMON_SDIST_PRESENT]
238250
sdist_absent = [*_COMMON_SDIST_ABSENT, *ext_defaults.get("sdist_absent_extra", [])]
239251
wheel_present = [module]
240252
wheel_absent = [*_COMMON_WHEEL_ABSENT, *ext_defaults.get("wheel_absent_extra", [])]
@@ -245,6 +257,50 @@ def copier_defaults(copier_config: dict) -> dict | None:
245257
}
246258

247259

260+
def _filter_extras_by_hatch(extras: list[str], hatch_config: dict) -> list[str]:
261+
"""Filter copier sdist_present_extra against the hatch sdist config.
262+
263+
Uses the same precedence as hatchling:
264+
1. ``only-include`` — exhaustive; keep extras that appear in the list.
265+
2. ``packages`` (when no ``only-include``) — acts as ``only-include``
266+
fallback; keep extras that appear in the list.
267+
3. ``include`` — keep extras that match an include pattern.
268+
4. ``force-include`` — destinations are always present; keep extras
269+
whose path appears as a force-include destination.
270+
5. No constraints — keep everything.
271+
"""
272+
sdist_cfg = hatch_config.get("targets", {}).get("sdist", {})
273+
only_include = sdist_cfg.get("only-include")
274+
packages = sdist_cfg.get("packages")
275+
includes = sdist_cfg.get("include")
276+
force_include = sdist_cfg.get("force-include") or hatch_config.get("force-include") or {}
277+
278+
# Collect the set of paths that will appear in the sdist.
279+
allowed: set[str] | None = None
280+
281+
if only_include is not None:
282+
allowed = set(only_include)
283+
elif packages is not None:
284+
# Hatch treats packages as only-include (explicit walk).
285+
# include is bypassed during an explicit walk.
286+
allowed = set(packages)
287+
288+
# force-include destinations are always present.
289+
force_paths = {v.strip("/") for v in force_include.values()}
290+
291+
if allowed is not None:
292+
allowed |= force_paths
293+
return [p for p in extras if p in allowed]
294+
295+
# include-only (no packages, no only-include): filter extras.
296+
if includes is not None:
297+
include_set = set(includes) | force_paths
298+
return [p for p in extras if p in include_set]
299+
300+
# No restrictions — keep all extras.
301+
return extras
302+
303+
248304
# ── Building ──────────────────────────────────────────────────────────
249305

250306

@@ -295,6 +351,8 @@ def find_dist_files(output_dir: str) -> tuple[str | None, str | None]:
295351
"""Return ``(sdist_path, wheel_path)`` found in *output_dir*."""
296352
sdist_path = None
297353
wheel_path = None
354+
if not os.path.isdir(output_dir):
355+
return None, None
298356
for name in os.listdir(output_dir):
299357
if name.endswith((".tar.gz", ".zip")):
300358
sdist_path = os.path.join(output_dir, name)
@@ -303,6 +361,23 @@ def find_dist_files(output_dir: str) -> tuple[str | None, str | None]:
303361
return sdist_path, wheel_path
304362

305363

364+
_DEFAULT_DIST_DIRS = ["dist", "wheelhouse"]
365+
366+
367+
def _find_pre_built(source_dir: str) -> str | None:
368+
"""Search default directories for pre-built distributions.
369+
370+
Checks ``dist/`` and ``wheelhouse/`` under *source_dir*. Returns the
371+
first directory that contains at least one sdist or wheel, or ``None``.
372+
"""
373+
for dirname in _DEFAULT_DIST_DIRS:
374+
candidate = os.path.join(source_dir, dirname)
375+
sdist, wheel = find_dist_files(candidate)
376+
if sdist or wheel:
377+
return candidate
378+
return None
379+
380+
306381
# ── Listing files ─────────────────────────────────────────────────────
307382

308383

@@ -470,44 +545,63 @@ def _sdist_expected_files(vcs_files: list[str], hatch_config: dict) -> set[str]:
470545
"""Derive the set of VCS files we expect to see in the sdist,
471546
taking ``[tool.hatch.build.targets.sdist]`` into account.
472547
473-
This returns files under the declared ``packages``, ``only-include``,
474-
or ``include`` patterns — i.e. the *source* files that must be present.
475-
Top-level metadata files (pyproject.toml, README, LICENSE, …) are
476-
intentionally left to the ``present``/``absent`` checks in the user config.
548+
Follows hatchling's precedence:
549+
550+
1. ``only-include`` is an exhaustive list of paths to walk.
551+
2. If absent, ``packages`` is used as ``only-include`` (hatch fallback).
552+
3. If neither is set and ``include`` is present, only files matching
553+
``include`` patterns are kept.
554+
4. ``exclude`` always removes files (except force-included ones).
555+
5. ``force-include`` entries are always present regardless of other
556+
settings. Hatch also auto-force-includes ``pyproject.toml``,
557+
``.gitignore``, README, and LICENSE files.
477558
"""
478559
sdist_cfg = hatch_config.get("targets", {}).get("sdist", {})
479560
only_include = sdist_cfg.get("only-include")
480561
packages = sdist_cfg.get("packages")
481562
includes = sdist_cfg.get("include", [])
482563
excludes = sdist_cfg.get("exclude", [])
564+
force_include = sdist_cfg.get("force-include", {})
483565

484-
# Determine scan paths following hatch's precedence:
485-
# only-include > packages > (everything)
486-
if only_include is not None:
487-
scan_paths = only_include
488-
elif packages is not None:
489-
scan_paths = packages
490-
else:
491-
scan_paths = None
566+
# Also pick up global-level force-include (target overrides global,
567+
# but if only global is set, use it).
568+
if not force_include:
569+
force_include = hatch_config.get("force-include", {})
570+
571+
# Step 1: Determine base set from only-include / packages / include.
572+
# Hatch's fallback: only_include = configured or (packages or [])
573+
# When truthy, only those directory roots are walked.
574+
scan_paths = only_include if only_include is not None else packages
492575

493576
expected = set()
494-
for f in vcs_files:
495-
if scan_paths is not None:
496-
under_path = any(f == p or f.startswith(p.rstrip("/") + "/") for p in scan_paths)
497-
if under_path:
577+
if scan_paths is not None:
578+
for f in vcs_files:
579+
if any(f == p or f.startswith(p.rstrip("/") + "/") for p in scan_paths):
498580
expected.add(f)
499-
else:
500-
# No explicit restrictions – everything in VCS is expected
501-
expected.add(f)
502-
503-
if includes:
504-
# TODO:
505-
pass
581+
elif includes:
582+
# No only-include or packages: full tree walk, but include
583+
# patterns act as a filter.
584+
for f in vcs_files:
585+
if any(_matches_hatch_pattern(f, inc) for inc in includes):
586+
expected.add(f)
587+
else:
588+
# No restrictions — everything in VCS is expected.
589+
expected = set(vcs_files)
506590

507-
# Apply excludes
591+
# Step 2: Apply excludes (never affects force-include).
508592
if excludes:
509593
expected = {f for f in expected if not any(_matches_hatch_pattern(f, exc) for exc in excludes)}
510594

595+
# Step 3: Add force-include destinations.
596+
# force-include is {source: dest} — we care about the dest paths
597+
# since those are what appear in the archive.
598+
for dest in force_include.values():
599+
dest = dest.strip("/")
600+
# If the dest matches a VCS file, add it.
601+
for f in vcs_files:
602+
if f == dest or f.startswith(dest.rstrip("/") + "/"):
603+
expected.add(f)
604+
511605
return expected
512606

513607

@@ -567,6 +661,7 @@ def check_dist(
567661
no_isolation: bool = False,
568662
verbose: bool = False,
569663
pre_built: str | None = None,
664+
rebuild: bool = False,
570665
) -> tuple[bool, list[str]]:
571666
"""Run all distribution checks.
572667
@@ -582,6 +677,9 @@ def check_dist(
582677
If given, skip building and use existing dist files from this
583678
directory. Useful when native toolchains have already produced
584679
the archives.
680+
rebuild:
681+
Force a fresh build even when pre-built distributions exist in
682+
``dist/`` or ``wheelhouse/``.
585683
586684
Returns ``(success, messages)``.
587685
"""
@@ -597,7 +695,20 @@ def check_dist(
597695
dist_dir = os.path.abspath(pre_built)
598696
messages.append(f"Using pre-built distributions from {dist_dir}")
599697
sdist_path, wheel_path = find_dist_files(dist_dir)
698+
elif not rebuild:
699+
# Auto-detect pre-built dists in dist/ or wheelhouse/
700+
detected = _find_pre_built(source_dir)
701+
if detected is not None:
702+
dist_dir = detected
703+
messages.append(f"Using pre-built distributions from {dist_dir}")
704+
sdist_path, wheel_path = find_dist_files(dist_dir)
705+
pre_built = dist_dir # so downstream logic treats it as pre-built
706+
else:
707+
pre_built = None # fall through to build
600708
else:
709+
pre_built = None # --rebuild: ignore any existing dists
710+
711+
if pre_built is None:
601712
tmpdir_ctx = tempfile.TemporaryDirectory(prefix="check-dist-")
602713
tmpdir = tmpdir_ctx.__enter__()
603714
try:

0 commit comments

Comments
 (0)