@@ -26,23 +26,14 @@ def test_debug_op(self) -> None:
2626
2727
2828class TestHeaderDeps (unittest .TestCase ):
29- """Tests for the header-dependency tracking used to build
30- `Extension.depends`, which drives setuptools' `newer_group` decision
31- about whether to recompile a .o file on incremental builds.
32-
33- The critical case is cross-group export-table headers: each module's
34- `__native_internal_<mod>.h` does `#include <other_group/__native_other.h>`,
35- and the consumer's compiled .o file bakes in byte offsets into that
36- header's `export_table_<group>` struct. If we miss this header in the
37- deps list, struct-layout changes in `other_group` won't trigger a
38- rebuild of the consumer, and its baked-in offsets will silently resolve
39- to whatever now occupies those slots.
29+ """
30+ Tests for the header-dependency tracking used to build `Extension.depends`, which drives
31+ setuptools' `newer_group` decision about whether to recompile a .o file on incremental builds.
4032 """
4133
4234 def test_get_header_deps_quoted_includes (self ) -> None :
43- # Quoted includes — the historical form. Used by the .c file to
44- # reach its own __native_<mod>.h / __native_internal_<mod>.h. The
45- # `False` in each tuple marks the include as non-angled, which
35+ # Quoted includes: the historical form. Used by the .c file to reach its own __native_<mod>.h /
36+ # __native_internal_<mod>.h. The `False` in each tuple marks the include as non-angled, which
4637 # `resolve_cfile_deps` uses to search the includer's directory.
4738 cfile = '#include "__native_caller.h"\n #include "__native_internal_caller.h"\n '
4839 assert get_header_deps ([("caller.c" , cfile )]) == [
@@ -51,51 +42,46 @@ def test_get_header_deps_quoted_includes(self) -> None:
5142 ]
5243
5344 def test_get_header_deps_angle_bracket_includes (self ) -> None :
54- # Angle-bracket includes are also matched, and reported with
55- # is_angled=True so that the resolver skips the includer's dir
56- # for them (matching the C preprocessor). The cross-group export
57- # header is reached via `#include <other_group/__native_other.h>`
58- # in __native_internal_<mod>.h. Before this was matched the dep
59- # was missed entirely and the consumer's .o was never invalidated
60- # when the other group's struct layout shifted.
45+ # Angle-bracket includes are also matched, and reported with is_angled=True so that the resolver
46+ # skips the includer's dir for them (matching the C preprocessor). The cross-group export header
47+ # is reached via `#include <other_group/__native_other.h>` in __native_internal_<mod>.h. Before
48+ # this was matched the dep was missed entirely and the consumer's .o was never invalidated when
49+ # the other group's struct layout shifted.
6150 cfile = "#include <Python.h>\n #include <lib/__native_functions.h>\n "
6251 assert get_header_deps ([("caller.c" , cfile )]) == [
6352 (True , "Python.h" ),
6453 (True , "lib/__native_functions.h" ),
6554 ]
6655
6756 def test_get_header_deps_mixed_and_whitespace (self ) -> None :
68- # The preprocessor tolerates whitespace and the leading-hash form.
69- # `get_header_deps` returns sorted tuples — non-angled (False) sorts
70- # before angled (True), then alphabetical within each kind.
57+ # The preprocessor tolerates whitespace and the leading-hash form. `get_header_deps` returns sorted
58+ # tuples — non-angled (False) sorts before angled (True), then alphabetical within each kind.
7159 cfile = '# include "a.h"\n # include <b.h>\n #include\t "c.h"\n '
7260 assert get_header_deps ([("x.c" , cfile )]) == [(False , "a.h" ), (False , "c.h" ), (True , "b.h" )]
7361
7462 def test_resolve_walks_transitively_through_headers (self ) -> None :
75- # Reproduces the bug scenario: caller's .c only directly includes
76- # caller's own headers, but caller's __native_internal_caller.h
77- # includes the cross-group export header. The resolver must follow
78- # that chain so setuptools sees the cross-group header as a dep.
63+ # Reproduces the bug scenario: caller's .c only directly includes caller's own headers, but
64+ # caller's __native_internal_caller.h includes the cross-group export header. The resolver
65+ # must follow that chain so setuptools sees the cross-group header as a dep.
7966 with tempfile .TemporaryDirectory () as tmp :
8067 build_dir = tmp
8168 os .makedirs (os .path .join (build_dir , "lib" ))
8269 os .makedirs (os .path .join (build_dir , "other_group" ))
8370
84- # caller.c's directly-included headers — both live alongside
71+ # caller.c's directly-included headers, both live alongside
8572 # caller.c under build/ (resolved via target_dir).
8673 internal_h = os .path .join (build_dir , "__native_internal_caller.h" )
8774 caller_h = os .path .join (build_dir , "__native_caller.h" )
8875 cross_group_h = os .path .join (build_dir , "lib" , "__native_functions.h" )
8976 unrelated_h = os .path .join (build_dir , "other_group" , "__native_other.h" )
9077
9178 with open (caller_h , "w" ) as f :
92- # lib-rt headers don't exist on disk under build/, so they
93- # get dropped during resolution and aren't recursed into.
79+ # Headers outside build/ (CPython's <Python.h>, lib-rt's <CPy.h>) don't resolve under
80+ # target_dir, so they get dropped during resolution and aren't recursed into.
9481 f .write ("#include <Python.h>\n #include <CPy.h>\n " )
9582 with open (internal_h , "w" ) as f :
96- # The smoking gun: this header includes a header in another
97- # group via angle brackets. Pre-fix, this dep was invisible
98- # to setuptools.
83+ # This header includes a header in another group via angle brackets. Pre-fix, this dep
84+ # was invisible to setuptools.
9985 f .write (
10086 "#include <Python.h>\n "
10187 '#include "__native_caller.h"\n '
@@ -104,15 +90,12 @@ def test_resolve_walks_transitively_through_headers(self) -> None:
10490 with open (cross_group_h , "w" ) as f :
10591 f .write ("struct export_table_lib___functions { int x; };\n " )
10692 with open (unrelated_h , "w" ) as f :
107- # Sibling group not reached from caller's chain — must
108- # NOT appear in the resolved set.
93+ # Sibling group not reached from caller's chain => must NOT appear in the resolved set.
10994 f .write ("struct unrelated { int x; };\n " )
11095
111- # caller.c is in build_dir, so its includer-dir is build_dir.
112- # Both directly-included headers are quoted (`False`); the
113- # cross-group header that __native_internal_caller.h reaches
114- # via `<lib/__native_functions.h>` is found by the recursive
115- # walk re-reading the on-disk header.
96+ # caller.c is in build_dir, so its includer-dir is build_dir. Both directly-included headers
97+ # are quoted (`False`); the cross-group header that __native_internal_caller.h reaches via
98+ # `<lib/__native_functions.h>` is found by the recursive walk re-reading the on-disk header.
11699 deps = resolve_cfile_deps (
117100 cfile_dir = build_dir ,
118101 direct_includes = [
@@ -128,11 +111,9 @@ def test_resolve_walks_transitively_through_headers(self) -> None:
128111 )
129112
130113 def test_resolve_drops_unresolvable_includes (self ) -> None :
131- # `<Python.h>`, `<CPy.h>`, etc. don't live under target_dir, so
132- # they're dropped from depends. They never change between builds,
133- # so this is the right behavior — and crucially it stops
134- # setuptools' `missing="newer"` from treating them as always-newer
135- # and force-rebuilding every translation unit.
114+ # `<Python.h>`, `<CPy.h>`, etc. don't live under target_dir, so they're dropped from depends. They
115+ # never change between builds, so this is the right behavior. Crucially, it stops setuptools'
116+ # `missing="newer"` from treating them as always-newer and force-rebuilding every translation unit.
136117 with tempfile .TemporaryDirectory () as tmp :
137118 cfile_dir = tmp
138119 deps = resolve_cfile_deps (
@@ -143,11 +124,9 @@ def test_resolve_drops_unresolvable_includes(self) -> None:
143124 assert deps == set ()
144125
145126 def test_resolve_search_order_matches_preprocessor (self ) -> None :
146- # When the same header name exists both next to the includer and
147- # under target_dir, the C preprocessor picks the includer-dir copy
148- # for `#include "shared.h"` and the target_dir copy for
149- # `#include <shared.h>`. The resolver must record the same path
150- # the compiler will actually consume, otherwise mtimes of the
127+ # When the same header name exists both next to the includer and under target_dir, the C preprocessor
128+ # picks the includer-dir copy for `#include "shared.h"` and the target_dir copy for `#include <shared.h>`.
129+ # The resolver must record the same path the compiler will actually consume, otherwise mtimes of the
151130 # wrong file drive incremental rebuild decisions.
152131 with tempfile .TemporaryDirectory () as tmp :
153132 includer = os .path .join (tmp , "groupA" )
0 commit comments