44
55import fnmatch
66import os
7+ import re
78import subprocess
89import sys
910import tarfile
1011import tempfile
1112import zipfile
1213from pathlib import Path
1314
15+ import yaml
16+
1417if sys .version_info >= (3 , 11 ):
1518 import tomllib
1619else :
@@ -65,20 +68,42 @@ def _wrong_platform_extensions() -> list[str]:
6568 return list (mapping .keys ())
6669
6770
68- def load_config (pyproject_path : str | Path = "pyproject.toml" ) -> dict :
69- """Load ``[tool.check-dist]`` configuration from *pyproject.toml*."""
71+ def load_config (pyproject_path : str | Path = "pyproject.toml" , * , source_dir : str | Path | None = None ) -> dict :
72+ """Load ``[tool.check-dist]`` configuration from *pyproject.toml*.
73+
74+ If no ``[tool.check-dist]`` section exists and *source_dir* contains a
75+ ``.copier-answers.yaml`` with an ``add_extension`` key, sensible
76+ defaults are derived from the copier template answers.
77+ """
7078 path = Path (pyproject_path )
79+ empty = {
80+ "sdist" : {"present" : [], "absent" : []},
81+ "wheel" : {"present" : [], "absent" : []},
82+ }
7183 if not path .exists ():
72- return {
73- "all" : {"present" : [], "absent" : []},
74- "sdist" : {"present" : [], "absent" : []},
75- "wheel" : {"present" : [], "absent" : []},
76- }
84+ # No pyproject.toml at all — try copier defaults
85+ if source_dir is not None :
86+ copier_cfg = load_copier_config (source_dir )
87+ defaults = copier_defaults (copier_cfg )
88+ if defaults is not None :
89+ return defaults
90+ return empty
7791
7892 with open (path , "rb" ) as f :
7993 config = tomllib .load (f )
8094
8195 cd = config .get ("tool" , {}).get ("check-dist" , {})
96+
97+ # If there's no [tool.check-dist] section at all, try copier defaults
98+ if not cd :
99+ if source_dir is None :
100+ source_dir = path .parent
101+ copier_cfg = load_copier_config (source_dir )
102+ defaults = copier_defaults (copier_cfg )
103+ if defaults is not None :
104+ return defaults
105+ return empty
106+
82107 base_present = cd .get ("present" , [])
83108 base_absent = cd .get ("absent" , [])
84109 sdist_cfg = cd .get ("sdist" , {})
@@ -108,6 +133,118 @@ def load_hatch_config(pyproject_path: str | Path = "pyproject.toml") -> dict:
108133 return config .get ("tool" , {}).get ("hatch" , {}).get ("build" , {})
109134
110135
136+ # ── Copier template defaults ─────────────────────────────────────────
137+
138+ # Per-extension type defaults for sdist/wheel present/absent patterns.
139+ # Keys follow the ``add_extension`` value in ``.copier-answers.yaml``.
140+ _EXTENSION_DEFAULTS : dict [str , dict ] = {
141+ "cpp" : {
142+ "sdist_present_extra" : ["cpp" ],
143+ "sdist_absent_extra" : [".clang-format" ],
144+ "wheel_absent_extra" : ["cpp" ],
145+ },
146+ "rust" : {
147+ "sdist_present_extra" : ["rust" , "src" , "Cargo.toml" , "Cargo.lock" ],
148+ "sdist_absent_extra" : [".gitattributes" , "target" ],
149+ "wheel_absent_extra" : ["rust" , "src" , "Cargo.toml" ],
150+ },
151+ "js" : {
152+ "sdist_present_extra" : ["js" ],
153+ "sdist_absent_extra" : [".gitattributes" , ".vscode" ],
154+ "wheel_absent_extra" : ["js" ],
155+ },
156+ "jupyter" : {
157+ "sdist_present_extra" : ["js" ],
158+ "sdist_absent_extra" : [".gitattributes" , ".vscode" ],
159+ "wheel_absent_extra" : ["js" ],
160+ },
161+ "rustjswasm" : {
162+ "sdist_present_extra" : ["js" , "rust" , "src" , "Cargo.toml" , "Cargo.lock" ],
163+ "sdist_absent_extra" : [".gitattributes" , ".vscode" , "target" ],
164+ "wheel_absent_extra" : ["js" , "rust" , "src" , "Cargo.toml" ],
165+ },
166+ "cppjswasm" : {
167+ "sdist_present_extra" : ["cpp" , "js" ],
168+ "sdist_absent_extra" : [".clang-format" , ".vscode" ],
169+ "wheel_absent_extra" : ["js" , "cpp" ],
170+ },
171+ "python" : {
172+ "sdist_present_extra" : [],
173+ "sdist_absent_extra" : [],
174+ "wheel_absent_extra" : [],
175+ },
176+ }
177+
178+ # Common patterns shared across all extension types.
179+ _COMMON_SDIST_PRESENT = ["LICENSE" , "pyproject.toml" , "README.md" ]
180+ _COMMON_SDIST_ABSENT = [
181+ ".copier-answers.yaml" ,
182+ "Makefile" ,
183+ ".github" ,
184+ "dist" ,
185+ "docs" ,
186+ "examples" ,
187+ "tests" ,
188+ ]
189+ _COMMON_WHEEL_ABSENT = [
190+ ".gitignore" ,
191+ ".copier-answers.yaml" ,
192+ "Makefile" ,
193+ "pyproject.toml" ,
194+ ".github" ,
195+ "dist" ,
196+ "docs" ,
197+ "examples" ,
198+ "tests" ,
199+ ]
200+
201+
202+ def load_copier_config (source_dir : str | Path ) -> dict :
203+ """Load ``.copier-answers.yaml`` from *source_dir*, if it exists."""
204+ path = Path (source_dir ) / ".copier-answers.yaml"
205+ if not path .exists ():
206+ return {}
207+ with open (path ) as f :
208+ return yaml .safe_load (f ) or {}
209+
210+
211+ def _module_name_from_project (project_name : str ) -> str :
212+ """Convert a human project name to a Python module name.
213+
214+ Replaces spaces and hyphens with underscores.
215+ """
216+ return re .sub (r"[\s-]+" , "_" , project_name ).strip ("_" )
217+
218+
219+ def copier_defaults (copier_config : dict ) -> dict | None :
220+ """Derive default check-dist config from copier answers.
221+
222+ Returns a config dict with the same shape as ``load_config`` output,
223+ or ``None`` if deriving defaults is not possible (no ``add_extension``
224+ key, or unknown extension type).
225+ """
226+ extension = copier_config .get ("add_extension" )
227+ project_name = copier_config .get ("project_name" )
228+ if not extension or not project_name :
229+ return None
230+
231+ ext_defaults = _EXTENSION_DEFAULTS .get (extension )
232+ if ext_defaults is None :
233+ return None
234+
235+ module = _module_name_from_project (project_name )
236+
237+ sdist_present = [module , * ext_defaults .get ("sdist_present_extra" , []), * _COMMON_SDIST_PRESENT ]
238+ sdist_absent = [* _COMMON_SDIST_ABSENT , * ext_defaults .get ("sdist_absent_extra" , [])]
239+ wheel_present = [module ]
240+ wheel_absent = [* _COMMON_WHEEL_ABSENT , * ext_defaults .get ("wheel_absent_extra" , [])]
241+
242+ return {
243+ "sdist" : {"present" : sdist_present , "absent" : sdist_absent },
244+ "wheel" : {"present" : wheel_present , "absent" : wheel_absent },
245+ }
246+
247+
111248# ── Building ──────────────────────────────────────────────────────────
112249
113250
@@ -381,6 +518,7 @@ def check_sdist_vs_vcs(
381518 sdist_files : list [str ],
382519 vcs_files : list [str ],
383520 hatch_config : dict ,
521+ sdist_absent : list [str ] | None = None ,
384522) -> list [str ]:
385523 """Compare sdist contents against VCS-tracked files."""
386524 errors : list [str ] = []
@@ -404,11 +542,19 @@ def check_sdist_vs_vcs(
404542 missing = sorted (expected - sdist_set )
405543 # Filter common non-issues (dotfiles like .gitattributes)
406544 missing = [f for f in missing if not f .startswith ("." )]
545+ # Filter files that match the user's sdist absent patterns —
546+ # if a file is explicitly expected to be absent, it's not "missing".
547+ # Always include the common absent patterns (docs, tests, etc.) since
548+ # most build systems exclude these from sdists.
549+ all_absent = list (_COMMON_SDIST_ABSENT )
550+ if sdist_absent :
551+ all_absent .extend (sdist_absent )
552+ missing = [f for f in missing if not any (matches_pattern (f , pat ) for pat in all_absent )]
407553
408554 if extra :
409- errors .append (f"sdist contains files not tracked by VCS: { ', ' .join (extra )} " )
555+ errors .append (" \n sdist contains files not tracked by VCS:\n \t " + " \n \t " .join (extra ))
410556 if missing :
411- errors .append (f"VCS -tracked files missing from sdist: { ', ' .join (missing )} " )
557+ errors .append (" \n VCS -tracked files missing from sdist: \n \t " + " \n \t " .join (missing ))
412558 return errors
413559
414560
@@ -444,7 +590,7 @@ def check_dist(
444590 source_dir = os .path .abspath (source_dir )
445591
446592 pyproject_path = os .path .join (source_dir , "pyproject.toml" )
447- config = load_config (pyproject_path )
593+ config = load_config (pyproject_path , source_dir = source_dir )
448594 hatch_config = load_hatch_config (pyproject_path )
449595
450596 if pre_built is not None :
@@ -483,7 +629,7 @@ def check_dist(
483629
484630 try :
485631 vcs_files = get_vcs_files (source_dir )
486- errors .extend (check_sdist_vs_vcs (sdist_files , vcs_files , hatch_config ))
632+ errors .extend (check_sdist_vs_vcs (sdist_files , vcs_files , hatch_config , sdist_absent = config [ "sdist" ][ "absent" ] ))
487633 except CheckDistError as exc :
488634 messages .append (f" Warning: could not compare against VCS: { exc } " )
489635
0 commit comments