Skip to content

Commit ca6b291

Browse files
committed
Clean up comments
1 parent bfe6d8f commit ca6b291

2 files changed

Lines changed: 66 additions & 93 deletions

File tree

mypyc/build.py

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -619,38 +619,34 @@ def get_header_deps(cfiles: list[tuple[str, str]]) -> list[tuple[bool, str]]:
619619
def resolve_cfile_deps(
620620
cfile_dir: str, direct_includes: list[tuple[bool, str]], target_dir: str
621621
) -> set[str]:
622-
"""Resolve a .c file's `#include` directives to on-disk paths, walking
623-
transitively through resolved headers.
624-
625-
The C preprocessor resolves `#include "foo"` against the includer's
626-
directory first, then via -I, while `#include <foo>` only uses -I. We
627-
mirror that exactly: quoted includes are searched in (includer_dir,
628-
target_dir) order, and angled includes are searched in target_dir only.
629-
`target_dir` is the only -I path that holds files we generate; anything
630-
we cannot resolve under it (or, for quoted form, the includer's dir) is
631-
dropped — other headers like `<Python.h>` and `<CPy.h>` live elsewhere
632-
and do not change between builds, so they are not real deps for
633-
incremental purposes.
634-
635-
The walk is transitive: each resolved header is opened and scanned for
636-
its own `#include` directives. Without this, cross-group export-table
637-
headers reached via `__native_internal_<mod>.h` (which includes
638-
`<other_group/__native_other.h>`) would be missed, and edits that shift
639-
struct offsets in `other_group` would not trigger a recompile of the
640-
consumer's .o file. Its baked-in offsets would then resolve to whatever
641-
class/function now occupies that slot — silent runtime corruption.
642-
643-
Returns a set of resolved paths suitable for use as an Extension.depends
644-
list.
622+
"""
623+
Resolve a .c file's `#include`s to on-disk paths, walking transitively through resolved headers.
624+
625+
The C preprocessor resolves `#include "foo"` against the includer's directory first, then via
626+
-I, while `#include <foo>` only uses -I. We mirror that exactly: quoted includes are searched
627+
in (includer_dir, target_dir) order, and angled includes are searched in target_dir only.
628+
`target_dir` is the only -I path that holds files we generate; anything we cannot resolve under
629+
it (or, for quoted form, the includer's dir) is dropped. Other headers like `<Python.h>` and
630+
`<CPy.h>` live elsewhere and do not change between builds, so they are not real dependencies
631+
for incremental purposes.
632+
633+
The walk is transitive: each resolved header is opened and scanned for its own `#include`
634+
directives. Without this, cross-group export-table headers reached via `__native_internal_<mod>.h`
635+
(which includes `<other_group/__native_other.h>`) would be missed, and edits that shift struct
636+
offsets in `other_group` would not trigger a recompile of the consumer's .o file. Its baked-in
637+
offsets would then resolve to whatever class/function now occupies that slot => runtime corruption.
638+
639+
Returns a set of resolved paths suitable for use as an Extension.depends list.
645640
"""
646641
resolved: set[str] = set()
647-
# Worklist of (search_dir, is_angled, header_name). search_dir is the
648-
# includer's directory — for the initial cfile it is the cfile's dir, for
649-
# a transitively-included header it is that header's dir. It is only
650-
# consulted for quoted-form includes.
642+
643+
# Worklist of (search_dir, is_angled, header_name). search_dir is the includer's directory; for the
644+
# initial cfile it is the cfile's dir, for a transitively-included header it is that header's dir.
645+
# It is only consulted for quoted-form includes.
651646
worklist: list[tuple[str, bool, str]] = [
652647
(cfile_dir, is_angled, dep) for is_angled, dep in direct_includes
653648
]
649+
654650
while worklist:
655651
search_dir, is_angled, dep = worklist.pop()
656652
# Quoted form: includer's dir first, then -I (target_dir).
@@ -663,13 +659,12 @@ def resolve_cfile_deps(
663659
if candidate in resolved:
664660
break
665661
resolved.add(candidate)
666-
# Recurse only into headers. Some lib-rt sources are pulled in
667-
# as `#include "init.c"` etc.; those do not resolve under
668-
# target_dir so they get filtered out before we would try to scan
662+
# Recurse only into headers. Some lib-rt sources are pulled in as `#include "init.c"` etc.;
663+
# those do not resolve under target_dir so they get filtered out before we would try to scan
669664
# them, but the .h guard is a cheap belt-and-braces.
670665
if candidate.endswith(".h"):
671666
try:
672-
with open(candidate, encoding="utf-8", errors="replace") as f:
667+
with open(candidate, encoding="utf-8") as f:
673668
header_contents = f.read()
674669
except OSError:
675670
header_contents = ""
@@ -731,23 +726,23 @@ def mypyc_build(
731726
# Write out the generated C and collect the files for each group.
732727
# Should this be here??
733728
#
734-
# Header resolution is deferred to a second pass: a header in one group
735-
# may include a header generated by another group, so resolving
736-
# immediately would miss cross-group deps for groups processed first.
729+
# Header resolution is deferred to a second pass: a header in one group may include a header
730+
# generated by another group, so resolving here misses cross-group deps for groups processed first.
737731
pending: list[list[tuple[str, list[tuple[bool, str]]]]] = []
738732
for cfiles in group_cfiles:
739733
per_cfile_deps: list[tuple[str, list[tuple[bool, str]]]] = []
740734
for cfile, ctext in cfiles:
741735
cfile = os.path.join(compiler_options.target_dir, cfile)
736+
742737
# Empty contents marks a file the previous run already wrote
743738
# (fully-cached group): skip the rewrite and just reuse it.
744739
if ctext and not options.mypyc_skip_c_generation:
745740
write_file(cfile, ctext)
746-
# For fully-cached groups ctext is empty; read the on-disk .c so
747-
# the dep resolver can walk its transitive header chain and populate
748-
# Extension.depends — otherwise cross-group export-table header
749-
# changes (e.g. a new class shifting struct offsets) won't trigger
750-
# a recompile of this cached consumer's .o.
741+
742+
# For fully-cached groups ctext is empty; read the on-disk .c so the dep resolver
743+
# can walk its transitive header chain and populate Extension.depends. Otherwise,
744+
# cross-group export-table header changes (e.g. a new class shifting struct offsets)
745+
# won't trigger a recompile of this cached consumer's .o.
751746
if not ctext and os.path.exists(cfile):
752747
try:
753748
with open(cfile, encoding="utf-8") as _f:
@@ -757,9 +752,8 @@ def mypyc_build(
757752
per_cfile_deps.append((cfile, get_header_deps([(cfile, ctext)])))
758753
pending.append(per_cfile_deps)
759754

760-
# Second pass: assemble each group's .c filenames and resolve transitive
761-
# deps now that every group's headers are on disk. See resolve_cfile_deps
762-
# for the rules.
755+
# Second pass: assemble each group's .c filenames and resolve transitive deps now that every group's
756+
# headers are on disk. See resolve_cfile_deps for the rules.
763757
group_cfilenames: list[tuple[list[str], list[str]]] = []
764758
for per_cfile in pending:
765759
cfilenames = [cf for cf, _ in per_cfile if os.path.splitext(cf)[1] == ".c"]

mypyc/test/test_misc.py

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,14 @@ def test_debug_op(self) -> None:
2626

2727

2828
class 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

Comments
 (0)