splatfold 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1911 @@
1
+ Metadata-Version: 2.4
2
+ Name: splatfold
3
+ Version: 0.2.0
4
+ Summary: Fold local Python wildcard imports into one standalone source file.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/WithSofie/splatfold
7
+ Project-URL: Repository, https://github.com/WithSofie/splatfold
8
+ Project-URL: Issues, https://github.com/WithSofie/splatfold/issues
9
+ Project-URL: Changelog, https://github.com/WithSofie/splatfold/blob/main/CHANGELOG.md
10
+ Keywords: bundler,include,preprocessor,single-file,source-code
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Classifier: Topic :: Software Development :: Code Generators
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest<9,>=8; extra == "test"
30
+ Provides-Extra: dev
31
+ Requires-Dist: coverage[toml]<8,>=7.6; extra == "dev"
32
+ Requires-Dist: mypy<1.19,>=1.14; extra == "dev"
33
+ Requires-Dist: pytest<9,>=8; extra == "dev"
34
+ Requires-Dist: ruff>=0.9; extra == "dev"
35
+ Provides-Extra: release
36
+ Requires-Dist: build>=1.2; extra == "release"
37
+ Requires-Dist: check-wheel-contents<1,>=0.6; extra == "release"
38
+ Requires-Dist: packaging>=24.2; extra == "release"
39
+ Requires-Dist: twine>=6; extra == "release"
40
+ Requires-Dist: validate-pyproject>=0.24; extra == "release"
41
+ Dynamic: license-file
42
+
43
+ # Splatfold User Manual
44
+
45
+ **Splatfold** is a single-file Python source preprocessor. It gives local
46
+ wildcard imports an additional build-time meaning while keeping the development
47
+ source valid, ordinary Python.
48
+
49
+ > Splat modules open. Fold them into one file.
50
+
51
+ Upgrading from the former Obtuse project? See the
52
+ [migration guide](https://github.com/WithSofie/splatfold/blob/main/MIGRATING.md)
53
+ for command compatibility and intentional safety changes.
54
+
55
+ It recursively expands:
56
+
57
+ ```python
58
+ from module import *
59
+ ```
60
+
61
+
62
+ into the source code of the referenced local Python file, eventually producing a single combined Python file that can be distributed or executed independently.
63
+
64
+ Conceptually, it is similar to the C/C++:
65
+
66
+ ```c
67
+ #include "module.c"
68
+ ```
69
+
70
+ mechanism.
71
+
72
+ However, during development, your project remains a normal multi-file Python project. IDEs, LSPs, autocomplete, static analyzers, type checkers, code navigation, and refactoring tools can continue to understand the project normally.
73
+
74
+ ## Installation
75
+
76
+ Splatfold requires Python 3.9 or newer. After the first PyPI release, install it
77
+ with:
78
+
79
+ ```zsh
80
+ python3 -m pip install splatfold
81
+ ```
82
+
83
+ For now, install a local checkout with:
84
+
85
+ ```zsh
86
+ python3 -m pip install -e .
87
+ ```
88
+
89
+ The project intentionally keeps its complete implementation in the standalone
90
+ `splatfold.py` module. It has no runtime dependencies outside the Python
91
+ standard library.
92
+
93
+ CI tests Splatfold on CPython 3.9 through 3.14. Splatfold uses the parser from
94
+ the interpreter that runs it, so that interpreter must understand every syntax
95
+ feature used by the input project. It validates and folds source; it does not
96
+ transpile newer Python syntax for older interpreters.
97
+
98
+ ## Command-line and Python APIs
99
+
100
+ The preferred command-line form uses a positional input:
101
+
102
+ ```zsh
103
+ splatfold main.py -o dist/app.py
104
+ ```
105
+
106
+ The original `-i` and `--input` spellings remain supported:
107
+
108
+ ```zsh
109
+ splatfold -i main.py -o dist/app.py
110
+ ```
111
+
112
+ Programmatic builds do not write automatically:
113
+
114
+ ```python
115
+ from splatfold import build
116
+
117
+ result = build("main.py", output="dist/app.py")
118
+ print(result.included_paths)
119
+ result.write()
120
+ ```
121
+
122
+ This separation makes validation and build-system integration side-effect free
123
+ until the caller explicitly writes the validated result.
124
+
125
+ `build()` always reads, resolves, renders, and compile-checks the complete
126
+ artifact before returning. Its keyword arguments correspond to the CLI's
127
+ resolution, strictness, guard, marker, and tracing options. The returned
128
+ `BuildResult` exposes:
129
+
130
+ - `source`: validated generated Python source;
131
+ - `included_paths`: absolute paths in render order, beginning with the entry;
132
+ - `unresolved_wildcards`: preserved imports as `(source_path, module)` pairs;
133
+ - `cycles`: skipped recursion edges as `(importer, target)` pairs;
134
+ - `output_path`: the absolute default or requested destination.
135
+
136
+ `BuildResult.write()` performs the only output mutation. It uses an atomic
137
+ same-directory replacement, refuses to overwrite any included source, and
138
+ revalidates the current `source` value immediately before writing. It returns
139
+ the destination path. `SplatfoldError` is the public base exception;
140
+ `ResolutionError` identifies unresolved imports in strict mode.
141
+
142
+ The CLI returns status `0` on success and status `2` for argument, resolution,
143
+ source, rendering, validation, or write errors. Diagnostics go to standard
144
+ error; `--list-deps` writes its machine-friendly path list to standard output.
145
+ Splatfold is still pre-1.0, so incompatible public-API changes require a minor
146
+ version increment and changelog entry.
147
+
148
+ ---
149
+
150
+ # 1. Basic Usage
151
+
152
+ Suppose your project looks like this:
153
+
154
+ ```text
155
+ project/
156
+ ├── main.py
157
+ ├── tools.py
158
+ └── utils.py
159
+ ```
160
+
161
+ `main.py`:
162
+
163
+ ```python
164
+ from tools import *
165
+
166
+ def main():
167
+ print(double("hello"))
168
+
169
+ if __name__ == "__main__":
170
+ main()
171
+ ```
172
+
173
+ `tools.py`:
174
+
175
+ ```python
176
+ from utils import *
177
+
178
+ def double(value):
179
+ return duplicate(value)
180
+ ```
181
+
182
+ `utils.py`:
183
+
184
+ ```python
185
+ def duplicate(value):
186
+ return value + value
187
+ ```
188
+
189
+ Run the following command from the `project/` directory:
190
+
191
+ ```zsh
192
+ splatfold main.py
193
+ ```
194
+
195
+ The preprocessor generates:
196
+
197
+ ```text
198
+ main.flat.py
199
+ ```
200
+
201
+ Both:
202
+
203
+ ```python
204
+ from tools import *
205
+ ```
206
+
207
+ and:
208
+
209
+ ```python
210
+ from utils import *
211
+ ```
212
+
213
+ are recursively replaced with their actual source code.
214
+
215
+ You can then run:
216
+
217
+ ```zsh
218
+ python3 main.flat.py
219
+ ```
220
+
221
+ ---
222
+
223
+ # 2. Basic Command Format
224
+
225
+ Preferred form:
226
+
227
+ ```zsh
228
+ splatfold INPUT [options]
229
+ ```
230
+
231
+ The most common command is:
232
+
233
+ ```zsh
234
+ splatfold main.py
235
+ ```
236
+
237
+ To specify the output file:
238
+
239
+ ```zsh
240
+ splatfold \
241
+ main.py \
242
+ -o dist/app.py
243
+ ```
244
+
245
+ ---
246
+
247
+ # 3. Positional input and `-i` / `--input`
248
+
249
+ The preferred positional argument specifies the entry-point Python file:
250
+
251
+ ```zsh
252
+ splatfold main.py
253
+ ```
254
+
255
+ For compatibility, `-i` and `--input` specify the same file:
256
+
257
+ Example:
258
+
259
+ ```zsh
260
+ splatfold -i main.py
261
+ ```
262
+
263
+ Equivalent to:
264
+
265
+ ```zsh
266
+ splatfold --input main.py
267
+ ```
268
+
269
+ The input file is the starting point of the dependency graph.
270
+
271
+ For example:
272
+
273
+ ```text
274
+ main.py
275
+ ↓
276
+ tools.py
277
+ ↓
278
+ utils.py
279
+ ```
280
+
281
+ Running:
282
+
283
+ ```zsh
284
+ splatfold -i main.py
285
+ ```
286
+
287
+ starts from `main.py` and recursively traces all expandable:
288
+
289
+ ```python
290
+ from ... import *
291
+ ```
292
+
293
+ statements.
294
+
295
+ ---
296
+
297
+ # 4. `-o` / `--output`
298
+
299
+ Specifies the generated output file.
300
+
301
+ Example:
302
+
303
+ ```zsh
304
+ splatfold \
305
+ -i main.py \
306
+ -o build/app.py
307
+ ```
308
+
309
+ This generates:
310
+
311
+ ```text
312
+ build/app.py
313
+ ```
314
+
315
+ If `-o` is omitted:
316
+
317
+ ```zsh
318
+ splatfold -i main.py
319
+ ```
320
+
321
+ then:
322
+
323
+ ```text
324
+ main.py
325
+ ```
326
+
327
+ generates:
328
+
329
+ ```text
330
+ main.flat.py
331
+ ```
332
+
333
+ Likewise:
334
+
335
+ ```text
336
+ server.py
337
+ ```
338
+
339
+ generates:
340
+
341
+ ```text
342
+ server.flat.py
343
+ ```
344
+
345
+ ---
346
+
347
+ # 5. Which Imports Are Expanded?
348
+
349
+ Only wildcard imports of the form:
350
+
351
+ ```python
352
+ from module import *
353
+ ```
354
+
355
+ are treated as include directives.
356
+
357
+ For example:
358
+
359
+ ```python
360
+ from tools import *
361
+ ```
362
+
363
+ is expanded if the preprocessor can resolve a local:
364
+
365
+ ```text
366
+ tools.py
367
+ ```
368
+
369
+ file.
370
+
371
+ ---
372
+
373
+ ## Imports That Are Expanded
374
+
375
+ ```python
376
+ from tools import *
377
+ ```
378
+
379
+ ```python
380
+ from package.tools import *
381
+ ```
382
+
383
+ ```python
384
+ from .tools import *
385
+ ```
386
+
387
+ ```python
388
+ from ..common import *
389
+ ```
390
+
391
+ ```python
392
+ from . import *
393
+ ```
394
+
395
+ An expanded wildcard import must occupy its own physical line. A trailing
396
+ comment is allowed:
397
+
398
+ ```python
399
+ from tools import * # folded by Splatfold
400
+ ```
401
+
402
+ Splatfold rejects semicolon-separated forms such as
403
+ `from tools import *; ready = True`, because replacing the whole physical line
404
+ would otherwise silently discard unrelated code. Future imports that Splatfold
405
+ hoists follow the same rule.
406
+
407
+ ---
408
+
409
+ ## Imports That Are Not Expanded
410
+
411
+ A normal import:
412
+
413
+ ```python
414
+ import tools
415
+ ```
416
+
417
+ is not expanded.
418
+
419
+ A named import:
420
+
421
+ ```python
422
+ from tools import helper
423
+ ```
424
+
425
+ is not expanded.
426
+
427
+ An aliased import:
428
+
429
+ ```python
430
+ import tools as t
431
+ ```
432
+
433
+ is not expanded.
434
+
435
+ These imports remain in the generated file unchanged.
436
+
437
+ ---
438
+
439
+ # 6. Standard Library and Third-Party Imports
440
+
441
+ By default, if:
442
+
443
+ ```python
444
+ from something import *
445
+ ```
446
+
447
+ cannot be resolved to a local Python source file, it is preserved unchanged.
448
+
449
+ For example:
450
+
451
+ ```python
452
+ from math import *
453
+ ```
454
+
455
+ remains:
456
+
457
+ ```python
458
+ from math import *
459
+ ```
460
+
461
+ in the generated output.
462
+
463
+ This allows normal standard-library and third-party imports to continue working.
464
+
465
+ ---
466
+
467
+ # 7. `--strict`
468
+
469
+ If you want every wildcard import to resolve to a local source file, use:
470
+
471
+ ```zsh
472
+ splatfold \
473
+ -i main.py \
474
+ --strict
475
+ ```
476
+
477
+ For example:
478
+
479
+ ```python
480
+ from nonexistent import *
481
+ ```
482
+
483
+ If no local:
484
+
485
+ ```text
486
+ nonexistent.py
487
+ ```
488
+
489
+ can be found, normal mode:
490
+
491
+ ```zsh
492
+ splatfold -i main.py
493
+ ```
494
+
495
+ preserves:
496
+
497
+ ```python
498
+ from nonexistent import *
499
+ ```
500
+
501
+ However:
502
+
503
+ ```zsh
504
+ splatfold \
505
+ -i main.py \
506
+ --strict
507
+ ```
508
+
509
+ reports an error and stops preprocessing.
510
+
511
+ ---
512
+
513
+ # 8. Module Resolution Rules
514
+
515
+ For:
516
+
517
+ ```python
518
+ from tools import *
519
+ ```
520
+
521
+ the preprocessor attempts to resolve:
522
+
523
+ ```text
524
+ tools/__init__.py
525
+ ```
526
+
527
+ or:
528
+
529
+ ```text
530
+ tools.py
531
+ ```
532
+
533
+ For:
534
+
535
+ ```python
536
+ from package.tools import *
537
+ ```
538
+
539
+ it attempts to resolve:
540
+
541
+ ```text
542
+ package/tools/__init__.py
543
+ ```
544
+
545
+ or:
546
+
547
+ ```text
548
+ package/tools.py
549
+ ```
550
+
551
+ This package-before-module order matches normal Python imports when both forms
552
+ exist on the same search-path entry.
553
+
554
+ ---
555
+
556
+ # 9. `-r` / `--root`
557
+
558
+ `--root` specifies the source root used for absolute local imports.
559
+
560
+ By default:
561
+
562
+ > root = directory containing the input file
563
+
564
+ For example:
565
+
566
+ ```text
567
+ project/
568
+ ├── splatfold.py
569
+ ├── main.py
570
+ └── lib/
571
+ └── tools.py
572
+ ```
573
+
574
+ If the source contains:
575
+
576
+ ```python
577
+ from lib.tools import *
578
+ ```
579
+
580
+ then:
581
+
582
+ ```zsh
583
+ splatfold -i main.py
584
+ ```
585
+
586
+ normally resolves it correctly.
587
+
588
+ ---
589
+
590
+ If your project instead looks like:
591
+
592
+ ```text
593
+ project/
594
+ ├── splatfold.py
595
+ ├── app/
596
+ │ └── main.py
597
+ └── src/
598
+ └── tools.py
599
+ ```
600
+
601
+ and:
602
+
603
+ ```python
604
+ from tools import *
605
+ ```
606
+
607
+ should resolve relative to:
608
+
609
+ ```text
610
+ src/
611
+ ```
612
+
613
+ use:
614
+
615
+ ```zsh
616
+ splatfold \
617
+ -i app/main.py \
618
+ --root src
619
+ ```
620
+
621
+ or the short form:
622
+
623
+ ```zsh
624
+ splatfold \
625
+ -i app/main.py \
626
+ -r src
627
+ ```
628
+
629
+ ---
630
+
631
+ # 10. `-I` / `--search-path`
632
+
633
+ Additional local module search directories can be supplied with:
634
+
635
+ ```text
636
+ -I / --search-path
637
+ ```
638
+
639
+ Example project:
640
+
641
+ ```text
642
+ project/
643
+ ├── splatfold.py
644
+ ├── main.py
645
+ ├── src/
646
+ │ └── tools.py
647
+ └── shared/
648
+ └── common.py
649
+ ```
650
+
651
+ Run:
652
+
653
+ ```zsh
654
+ splatfold \
655
+ -i main.py \
656
+ -I src \
657
+ -I shared
658
+ ```
659
+
660
+ `-I` may be specified multiple times.
661
+
662
+ Example:
663
+
664
+ ```zsh
665
+ splatfold \
666
+ -i main.py \
667
+ -I src \
668
+ -I shared \
669
+ -I vendor
670
+ ```
671
+
672
+ ---
673
+
674
+ # 11. Relative Imports
675
+
676
+ Normal Python relative imports are supported.
677
+
678
+ For example:
679
+
680
+ ```python
681
+ from .tools import *
682
+ ```
683
+
684
+ and:
685
+
686
+ ```python
687
+ from ..common import *
688
+ ```
689
+
690
+ can be resolved according to the source file's package location.
691
+
692
+ Example:
693
+
694
+ ```text
695
+ package/
696
+ ├── main.py
697
+ ├── tools.py
698
+ └── helpers/
699
+ └── common.py
700
+ ```
701
+
702
+ Modules may continue to use standard Python relative-import syntax.
703
+
704
+ ---
705
+
706
+ # 12. Circular Dependencies
707
+
708
+ Circular dependencies do not cause infinite recursion.
709
+
710
+ For example:
711
+
712
+ ```text
713
+ main.py
714
+ ↓
715
+ another.py
716
+ ↓
717
+ tools.py
718
+ ↓
719
+ another.py
720
+ ```
721
+
722
+ That is:
723
+
724
+ `main.py`
725
+
726
+ ```python
727
+ from another import *
728
+ ```
729
+
730
+ `another.py`
731
+
732
+ ```python
733
+ from tools import *
734
+ ```
735
+
736
+ `tools.py`
737
+
738
+ ```python
739
+ from another import *
740
+ ```
741
+
742
+ The preprocessor detects:
743
+
744
+ ```text
745
+ another → tools → another
746
+ ```
747
+
748
+ and stops recursively expanding that cycle.
749
+
750
+ Each source file is emitted at most once.
751
+
752
+ ---
753
+
754
+ # 13. Duplicate Dependencies
755
+
756
+ For example:
757
+
758
+ ```text
759
+ main.py
760
+ ├── a.py
761
+ │ └── common.py
762
+ └── b.py
763
+ └── common.py
764
+ ```
765
+
766
+ Even though:
767
+
768
+ ```text
769
+ common.py
770
+ ```
771
+
772
+ is referenced through two different paths, it is emitted only once.
773
+
774
+ This behavior is called:
775
+
776
+ ```text
777
+ include once
778
+ ```
779
+
780
+ ---
781
+
782
+ # 14. Default Source Markers
783
+
784
+ By default, the generated file contains structural markers such as:
785
+
786
+ ```python
787
+ # >>> splatfold: begin tools.py
788
+ ```
789
+
790
+ and:
791
+
792
+ ```python
793
+ # <<< splatfold: end tools.py
794
+ ```
795
+
796
+ These indicate which generated sections came from which dependency.
797
+
798
+ Example:
799
+
800
+ ```python
801
+ # >>> splatfold: begin tools.py
802
+
803
+ def double(value):
804
+ return value * 2
805
+
806
+ # <<< splatfold: end tools.py
807
+ ```
808
+
809
+ Circular or duplicate includes may also produce explanatory comments.
810
+
811
+ These markers are useful for:
812
+
813
+ * reading generated files;
814
+ * debugging;
815
+ * understanding dependencies;
816
+ * identifying source boundaries.
817
+
818
+ ---
819
+
820
+ # 15. `--no-markers`
821
+
822
+ To generate a cleaner output file:
823
+
824
+ ```zsh
825
+ splatfold \
826
+ -i main.py \
827
+ --no-markers
828
+ ```
829
+
830
+ This disables structural comments such as:
831
+
832
+ ```python
833
+ # >>> splatfold: begin ...
834
+ ```
835
+
836
+ and:
837
+
838
+ ```python
839
+ # <<< splatfold: end ...
840
+ ```
841
+
842
+ The actual Python code is unaffected.
843
+
844
+ ---
845
+
846
+ # 16. `--inline-source-map`
847
+
848
+ If you want each generated line to indicate which original file and line number it came from, use:
849
+
850
+ ```zsh
851
+ splatfold \
852
+ -i main.py \
853
+ --inline-source-map
854
+ ```
855
+
856
+ For example, if line 70 of:
857
+
858
+ ```text
859
+ main.py
860
+ ```
861
+
862
+ contains:
863
+
864
+ ```python
865
+ print("hello")
866
+ ```
867
+
868
+ the output becomes:
869
+
870
+ ```python
871
+ print("hello") # main.py 70
872
+ ```
873
+
874
+ Example:
875
+
876
+ ```python
877
+ def main(): # main.py 68
878
+ message = "hello" # main.py 69
879
+ print(message) # main.py 70
880
+ ```
881
+
882
+ Dependencies are traced in the same way:
883
+
884
+ ```python
885
+ def double(value): # tools.py 21
886
+ return value * 2 # tools.py 22
887
+ ```
888
+
889
+ This is useful for:
890
+
891
+ * debugging;
892
+ * inspecting flattened output;
893
+ * comparing generated code to source files;
894
+ * quickly locating original code.
895
+
896
+ ---
897
+
898
+ ## Inline Source Map Safety Rules
899
+
900
+ The preprocessor does not add inline comments where doing so would change Python semantics.
901
+
902
+ For example:
903
+
904
+ ```python
905
+ MESSAGE = """hello
906
+ world
907
+ """
908
+ ```
909
+
910
+ must not become:
911
+
912
+ ```python
913
+ MESSAGE = """hello # main.py 10
914
+ world # main.py 11
915
+ """
916
+ ```
917
+
918
+ because the inserted text would become part of the string.
919
+
920
+ Therefore, such lines are not forcibly annotated.
921
+
922
+ ---
923
+
924
+ Likewise:
925
+
926
+ ```python
927
+ value = 1 + \
928
+ 2
929
+ ```
930
+
931
+ must not become:
932
+
933
+ ```python
934
+ value = 1 + \ # main.py 20
935
+ ```
936
+
937
+ because that would break Python syntax.
938
+
939
+ Therefore:
940
+
941
+ > `--inline-source-map` annotates source lines whenever it is safe to do so, but never at the cost of changing program behavior.
942
+
943
+ ---
944
+
945
+ # 17. `--source-marker`
946
+
947
+ The second source-tracing mode is:
948
+
949
+ ```zsh
950
+ splatfold \
951
+ -i main.py \
952
+ --source-marker
953
+ ```
954
+
955
+ Instead of adding a comment after every line, it inserts source-location markers before contiguous source regions.
956
+
957
+ For example:
958
+
959
+ ```python
960
+ # >>> splatfold: source main.py:70
961
+ print("hello")
962
+ ```
963
+
964
+ Or:
965
+
966
+ ```python
967
+ # >>> splatfold: source main.py:68
968
+ def main():
969
+ message = "hello"
970
+ print(message)
971
+ ```
972
+
973
+ This indicates that the first source line after the marker corresponds to:
974
+
975
+ ```text
976
+ main.py:68
977
+ ```
978
+
979
+ and subsequent uninterrupted source lines continue from there.
980
+
981
+ ---
982
+
983
+ If preprocessing switches to another dependency:
984
+
985
+ ```python
986
+ # >>> splatfold: source main.py:20
987
+
988
+ def foo():
989
+ pass
990
+
991
+ # >>> splatfold: source tools.py:1
992
+
993
+ def helper():
994
+ pass
995
+
996
+ # >>> splatfold: source main.py:24
997
+
998
+ def bar():
999
+ pass
1000
+ ```
1001
+
1002
+ This representation is more compact than an inline source map.
1003
+
1004
+ ---
1005
+
1006
+ # 18. `--inline-source-map` vs. `--source-marker`
1007
+
1008
+ ### Inline Source Map
1009
+
1010
+ Use:
1011
+
1012
+ ```zsh
1013
+ --inline-source-map
1014
+ ```
1015
+
1016
+ Output:
1017
+
1018
+ ```python
1019
+ x = 10 # main.py 20
1020
+ y = 20 # main.py 21
1021
+ print(x + y) # main.py 22
1022
+ ```
1023
+
1024
+ Advantages:
1025
+
1026
+ * source location is visible on every annotated line;
1027
+ * very convenient for debugging.
1028
+
1029
+ Disadvantages:
1030
+
1031
+ * generated output is longer;
1032
+ * source comments are more visually intrusive.
1033
+
1034
+ ---
1035
+
1036
+ ### Source Marker
1037
+
1038
+ Use:
1039
+
1040
+ ```zsh
1041
+ --source-marker
1042
+ ```
1043
+
1044
+ Output:
1045
+
1046
+ ```python
1047
+ # >>> splatfold: source main.py:20
1048
+ x = 10
1049
+ y = 20
1050
+ print(x + y)
1051
+ ```
1052
+
1053
+ Advantages:
1054
+
1055
+ * cleaner output;
1056
+ * closer to the original source;
1057
+ * source provenance is still preserved.
1058
+
1059
+ For normal inspection, prefer:
1060
+
1061
+ ```zsh
1062
+ --source-marker
1063
+ ```
1064
+
1065
+ For detailed debugging, prefer:
1066
+
1067
+ ```zsh
1068
+ --inline-source-map
1069
+ ```
1070
+
1071
+ ---
1072
+
1073
+ # 19. The Two Source-Tracing Modes Are Mutually Exclusive
1074
+
1075
+ Do not use:
1076
+
1077
+ ```zsh
1078
+ splatfold \
1079
+ -i main.py \
1080
+ --inline-source-map \
1081
+ --source-marker
1082
+ ```
1083
+
1084
+ at the same time.
1085
+
1086
+ These options are mutually exclusive.
1087
+
1088
+ Choose one source-tracing mode.
1089
+
1090
+ ---
1091
+
1092
+ # 20. `--source-marker` with `--no-markers`
1093
+
1094
+ This is a useful combination:
1095
+
1096
+ ```zsh
1097
+ splatfold \
1098
+ -i main.py \
1099
+ --source-marker \
1100
+ --no-markers
1101
+ ```
1102
+
1103
+ Structural markers such as:
1104
+
1105
+ ```python
1106
+ # >>> splatfold: begin tools.py
1107
+ ```
1108
+
1109
+ are disabled.
1110
+
1111
+ However, source-location markers such as:
1112
+
1113
+ ```python
1114
+ # >>> splatfold: source tools.py:20
1115
+ ```
1116
+
1117
+ are still generated.
1118
+
1119
+ This gives relatively clean output while retaining source tracing.
1120
+
1121
+ ---
1122
+
1123
+ # 21. `if __name__ == "__main__"`
1124
+
1125
+ Suppose a dependency:
1126
+
1127
+ ```text
1128
+ tools.py
1129
+ ```
1130
+
1131
+ contains:
1132
+
1133
+ ```python
1134
+ def helper():
1135
+ pass
1136
+
1137
+ if __name__ == "__main__":
1138
+ print("testing tools")
1139
+ ```
1140
+
1141
+ Under normal Python imports:
1142
+
1143
+ ```python
1144
+ from tools import *
1145
+ ```
1146
+
1147
+ the following code does not execute:
1148
+
1149
+ ```python
1150
+ print("testing tools")
1151
+ ```
1152
+
1153
+ Blindly flattening the entire file would change that behavior.
1154
+
1155
+ Therefore, by default, the preprocessor removes conventional top-level:
1156
+
1157
+ ```python
1158
+ if __name__ == "__main__":
1159
+ ```
1160
+
1161
+ blocks from dependency files.
1162
+
1163
+ However, the root input file's own:
1164
+
1165
+ ```python
1166
+ if __name__ == "__main__":
1167
+ main()
1168
+ ```
1169
+
1170
+ block is preserved.
1171
+
1172
+ A dependency guard with an `else` clause is not a conventional removable main
1173
+ guard: normal importing executes that `else` branch. Splatfold therefore fails
1174
+ with a clear error instead of silently deleting live code. Rewrite that module
1175
+ so import-time definitions live outside the guard, or use
1176
+ `--keep-main-guards` when literal inclusion is intentionally desired.
1177
+
1178
+ ---
1179
+
1180
+ # 22. `--keep-main-guards`
1181
+
1182
+ If you intentionally want dependency:
1183
+
1184
+ ```python
1185
+ if __name__ == "__main__":
1186
+ ```
1187
+
1188
+ blocks to remain in the output, use:
1189
+
1190
+ ```zsh
1191
+ splatfold \
1192
+ -i main.py \
1193
+ --keep-main-guards
1194
+ ```
1195
+
1196
+ This option is generally not recommended.
1197
+
1198
+ Use it only when you explicitly want behavior closer to literal textual inclusion.
1199
+
1200
+ ---
1201
+
1202
+ # 23. `from __future__ import ...`
1203
+
1204
+ The preprocessor automatically handles special imports such as:
1205
+
1206
+ ```python
1207
+ from __future__ import annotations
1208
+ ```
1209
+
1210
+ Python requires future imports to appear near the beginning of the module.
1211
+
1212
+ Therefore, if a dependency contains:
1213
+
1214
+ ```python
1215
+ from __future__ import annotations
1216
+ ```
1217
+
1218
+ the preprocessor does not simply copy it into the middle of the generated file.
1219
+
1220
+ Instead it:
1221
+
1222
+ 1. collects future imports;
1223
+ 2. deduplicates them;
1224
+ 3. removes them from their original positions;
1225
+ 4. places them in a valid position near the top of the final module.
1226
+
1227
+ For example:
1228
+
1229
+ ```python
1230
+ """Application."""
1231
+
1232
+ from __future__ import annotations
1233
+ ```
1234
+
1235
+ remains a valid module structure.
1236
+
1237
+ Users normally do not need to handle this manually.
1238
+
1239
+ ---
1240
+
1241
+ # 24. Source Encodings
1242
+
1243
+ The preprocessor uses Python's own source-encoding detection rules.
1244
+
1245
+ For example:
1246
+
1247
+ ```python
1248
+ # -*- coding: latin-1 -*-
1249
+ ```
1250
+
1251
+ is supported.
1252
+
1253
+ Different dependencies may use different encodings.
1254
+
1255
+ The final generated file is always written as:
1256
+
1257
+ ```text
1258
+ UTF-8
1259
+ ```
1260
+
1261
+ Therefore, source files do not need to be manually converted first.
1262
+
1263
+ ---
1264
+
1265
+ # 25. Shebang Handling
1266
+
1267
+ If the root input file begins with:
1268
+
1269
+ ```python
1270
+ #!/usr/bin/env python3
1271
+ ```
1272
+
1273
+ the shebang is preserved in the generated file.
1274
+
1275
+ Dependency shebangs are not treated as additional final-file shebangs.
1276
+
1277
+ ---
1278
+
1279
+ # 26. `--check-only`
1280
+
1281
+ To verify that the project can be flattened successfully without writing an output file, use:
1282
+
1283
+ ```zsh
1284
+ splatfold \
1285
+ -i main.py \
1286
+ --check-only
1287
+ ```
1288
+
1289
+ This still performs:
1290
+
1291
+ 1. dependency discovery;
1292
+ 2. source parsing;
1293
+ 3. circular dependency detection;
1294
+ 4. preprocessing;
1295
+ 5. flattening;
1296
+ 6. final Python syntax validation.
1297
+
1298
+ However, it does not write:
1299
+
1300
+ ```text
1301
+ main.flat.py
1302
+ ```
1303
+
1304
+ This is useful for validation before a build.
1305
+
1306
+ A recommended workflow is:
1307
+
1308
+ ```zsh
1309
+ splatfold \
1310
+ -i main.py \
1311
+ --check-only
1312
+ ```
1313
+
1314
+ and, if that succeeds:
1315
+
1316
+ ```zsh
1317
+ splatfold -i main.py
1318
+ ```
1319
+
1320
+ ---
1321
+
1322
+ # 27. `--list-deps`
1323
+
1324
+ To display the source files used by the build:
1325
+
1326
+ ```zsh
1327
+ splatfold \
1328
+ -i main.py \
1329
+ --list-deps
1330
+ ```
1331
+
1332
+ Example output:
1333
+
1334
+ ```text
1335
+ main.py
1336
+ another.py
1337
+ tools.py
1338
+ common.py
1339
+ ```
1340
+
1341
+ The files are shown in their actual processing/inclusion order.
1342
+
1343
+ This is useful for:
1344
+
1345
+ * checking dependencies;
1346
+ * detecting unexpectedly included files;
1347
+ * debugging module resolution.
1348
+
1349
+ ---
1350
+
1351
+ # 28. `-v` / `--verbose`
1352
+
1353
+ Enable verbose diagnostics:
1354
+
1355
+ ```zsh
1356
+ splatfold \
1357
+ -i main.py \
1358
+ -v
1359
+ ```
1360
+
1361
+ or:
1362
+
1363
+ ```zsh
1364
+ splatfold \
1365
+ -i main.py \
1366
+ --verbose
1367
+ ```
1368
+
1369
+ This prints additional information about dependency resolution, unresolved imports, circular dependencies, and related processing.
1370
+
1371
+ A useful debugging command is:
1372
+
1373
+ ```zsh
1374
+ splatfold \
1375
+ -i main.py \
1376
+ --check-only \
1377
+ --list-deps \
1378
+ -v
1379
+ ```
1380
+
1381
+ ---
1382
+
1383
+ # 29. Displaying the Version
1384
+
1385
+ Run:
1386
+
1387
+ ```zsh
1388
+ splatfold --version
1389
+ ```
1390
+
1391
+ Current version:
1392
+
1393
+ ```text
1394
+ splatfold 0.2.0
1395
+ ```
1396
+
1397
+ ---
1398
+
1399
+ # 30. Displaying CLI Help
1400
+
1401
+ Run:
1402
+
1403
+ ```zsh
1404
+ splatfold --help
1405
+ ```
1406
+
1407
+ or:
1408
+
1409
+ ```zsh
1410
+ splatfold -h
1411
+ ```
1412
+
1413
+ to display all available command-line options.
1414
+
1415
+ ---
1416
+
1417
+ # 31. Recommended Development Workflow
1418
+
1419
+ During development, keep the project as a normal multi-file Python project:
1420
+
1421
+ ```text
1422
+ project/
1423
+ ├── main.py
1424
+ ├── tools.py
1425
+ ├── parser.py
1426
+ ├── utils.py
1427
+ └── splatfold.py
1428
+ ```
1429
+
1430
+ Write normal Python imports such as:
1431
+
1432
+ ```python
1433
+ from tools import *
1434
+ from parser import *
1435
+ ```
1436
+
1437
+ Your IDE continues to treat them as regular Python modules.
1438
+
1439
+ ---
1440
+
1441
+ During development, run the original project normally:
1442
+
1443
+ ```zsh
1444
+ python3 main.py
1445
+ ```
1446
+
1447
+ ---
1448
+
1449
+ To validate flattening:
1450
+
1451
+ ```zsh
1452
+ splatfold \
1453
+ -i main.py \
1454
+ --check-only \
1455
+ --list-deps
1456
+ ```
1457
+
1458
+ ---
1459
+
1460
+ To create a debugging build:
1461
+
1462
+ ```zsh
1463
+ splatfold \
1464
+ -i main.py \
1465
+ --inline-source-map
1466
+ ```
1467
+
1468
+ ---
1469
+
1470
+ To create relatively clean output while retaining source tracing:
1471
+
1472
+ ```zsh
1473
+ splatfold \
1474
+ -i main.py \
1475
+ --source-marker \
1476
+ --no-markers
1477
+ ```
1478
+
1479
+ ---
1480
+
1481
+ For a clean release build:
1482
+
1483
+ ```zsh
1484
+ splatfold \
1485
+ -i main.py \
1486
+ -o dist/app.py \
1487
+ --no-markers
1488
+ ```
1489
+
1490
+ ---
1491
+
1492
+ Finally, test the generated file:
1493
+
1494
+ ```zsh
1495
+ python3 dist/app.py
1496
+ ```
1497
+
1498
+ ---
1499
+
1500
+ # 32. Recommended Debugging Command
1501
+
1502
+ If something goes wrong, first run:
1503
+
1504
+ ```zsh
1505
+ splatfold \
1506
+ -i main.py \
1507
+ --check-only \
1508
+ --list-deps \
1509
+ -v
1510
+ ```
1511
+
1512
+ This allows you to:
1513
+
1514
+ * avoid modifying any output file;
1515
+ * inspect dependencies;
1516
+ * inspect module resolution;
1517
+ * validate the final Python source.
1518
+
1519
+ If additional source-level tracing is needed:
1520
+
1521
+ ```zsh
1522
+ splatfold \
1523
+ -i main.py \
1524
+ --inline-source-map
1525
+ ```
1526
+
1527
+ Then inspect:
1528
+
1529
+ ```text
1530
+ main.flat.py
1531
+ ```
1532
+
1533
+ and its:
1534
+
1535
+ ```python
1536
+ # filename.py LINE
1537
+ ```
1538
+
1539
+ annotations.
1540
+
1541
+ ---
1542
+
1543
+ # 33. Syntax Errors
1544
+
1545
+ If any source file contains invalid Python syntax, for example:
1546
+
1547
+ ```python
1548
+ def hello(
1549
+ print("hello")
1550
+ ```
1551
+
1552
+ the preprocessor detects the error before generating the final output.
1553
+
1554
+ The error report includes:
1555
+
1556
+ * filename;
1557
+ * line number;
1558
+ * column number;
1559
+ * Python's syntax error message.
1560
+
1561
+ The final flattened output is also validated with:
1562
+
1563
+ ```python
1564
+ compile()
1565
+ ```
1566
+
1567
+ Therefore:
1568
+
1569
+ > Individual source files being valid does not guarantee that the flattened result is valid. The final output is checked again.
1570
+
1571
+ ---
1572
+
1573
+ # 34. The Preprocessor Does Not Execute Project Source Code
1574
+
1575
+ During preprocessing, the tool only:
1576
+
1577
+ * reads source code;
1578
+ * parses source code;
1579
+ * analyzes imports;
1580
+ * generates source code;
1581
+ * compile-checks source code.
1582
+
1583
+ It does not discover dependencies by executing:
1584
+
1585
+ ```python
1586
+ import your_module
1587
+ ```
1588
+
1589
+ and it does not use:
1590
+
1591
+ ```python
1592
+ exec(...)
1593
+ ```
1594
+
1595
+ to run project source code.
1596
+
1597
+ Therefore, preprocessing itself does not trigger module runtime side effects.
1598
+
1599
+ ---
1600
+
1601
+ # 35. Complete Example
1602
+
1603
+ Project:
1604
+
1605
+ ```text
1606
+ project/
1607
+ ├── splatfold.py
1608
+ ├── main.py
1609
+ ├── another.py
1610
+ └── tools.py
1611
+ ```
1612
+
1613
+ `main.py`:
1614
+
1615
+ ```python
1616
+ from another import *
1617
+
1618
+ def run():
1619
+ print(double_string("21"))
1620
+
1621
+ if __name__ == "__main__":
1622
+ run()
1623
+ ```
1624
+
1625
+ `another.py`:
1626
+
1627
+ ```python
1628
+ from tools import *
1629
+
1630
+ def is_string(value):
1631
+ return isinstance(value, str)
1632
+ ```
1633
+
1634
+ `tools.py`:
1635
+
1636
+ ```python
1637
+ from another import *
1638
+
1639
+ def double_string(value):
1640
+ if is_string(value):
1641
+ return str(int(value) * 2)
1642
+
1643
+ return value
1644
+ ```
1645
+
1646
+ This contains a circular dependency:
1647
+
1648
+ ```text
1649
+ another
1650
+ ↓
1651
+ tools
1652
+ ↓
1653
+ another
1654
+ ```
1655
+
1656
+ Run:
1657
+
1658
+ ```zsh
1659
+ splatfold \
1660
+ -i main.py \
1661
+ --source-marker
1662
+ ```
1663
+
1664
+ The preprocessor:
1665
+
1666
+ 1. reads `main.py`;
1667
+ 2. resolves `another.py`;
1668
+ 3. resolves `tools.py`;
1669
+ 4. encounters `another.py` again;
1670
+ 5. detects the cycle;
1671
+ 6. does not expand the same source file again;
1672
+ 7. generates a single:
1673
+
1674
+ ```text
1675
+ main.flat.py
1676
+ ```
1677
+ 8. validates the final Python syntax.
1678
+
1679
+ Then run:
1680
+
1681
+ ```zsh
1682
+ python3 main.flat.py
1683
+ ```
1684
+
1685
+ Expected output:
1686
+
1687
+ ```text
1688
+ 42
1689
+ ```
1690
+
1691
+ ---
1692
+
1693
+ # 36. When Should You Use This Tool?
1694
+
1695
+ This tool is suitable when you:
1696
+
1697
+ * want to develop a normal multi-file Python project;
1698
+ * want to distribute a single Python file;
1699
+ * want IDE and LSP support during development;
1700
+ * do not want to introduce a custom `#include` syntax;
1701
+ * are building small utilities;
1702
+ * are building single-file CLIs;
1703
+ * are building scripts;
1704
+ * are building plugins;
1705
+ * want easily distributable Python source;
1706
+ * are experimenting with compiler/preprocessor-style tooling.
1707
+
1708
+ ---
1709
+
1710
+ # 37. When Should You Not Use It?
1711
+
1712
+ If the application heavily depends on true Python module namespaces, for example:
1713
+
1714
+ ```python
1715
+ import foo
1716
+
1717
+ foo.value
1718
+ ```
1719
+
1720
+ or relies heavily on:
1721
+
1722
+ ```python
1723
+ __name__
1724
+ __package__
1725
+ __file__
1726
+ sys.modules
1727
+ ```
1728
+
1729
+ as well as complex:
1730
+
1731
+ * import hooks;
1732
+ * plugin systems;
1733
+ * dynamic imports;
1734
+ * module initialization side effects;
1735
+ * C extension modules;
1736
+
1737
+ then you should not assume that flattened behavior will always exactly match the original multi-module project.
1738
+
1739
+ Splatfold deliberately expands only module-level wildcard imports. Imports
1740
+ inside functions, classes, conditionals, and exception handlers remain normal
1741
+ Python imports.
1742
+
1743
+ Included modules share one generated global namespace. Unlike normal module
1744
+ imports, flattening does not preserve a private namespace per source file, and
1745
+ `__all__` does not hide definitions that are physically present in the combined
1746
+ file. Projects should therefore avoid conflicting top-level names and should
1747
+ not depend on module-specific metadata or initialization isolation.
1748
+
1749
+ Collected `from __future__ import ...` statements are hoisted to the generated
1750
+ module header, as Python requires. A future feature used by one dependency
1751
+ therefore applies to the entire generated module; projects should keep future
1752
+ feature choices consistent across their source tree.
1753
+
1754
+ This tool is a:
1755
+
1756
+ ```text
1757
+ source preprocessor / source flattener
1758
+ ```
1759
+
1760
+ not a complete:
1761
+
1762
+ ```text
1763
+ Python import system emulator
1764
+ ```
1765
+
1766
+ ---
1767
+
1768
+ # 38. Most Important Design Rule
1769
+
1770
+ Development source code should continue to use normal Python:
1771
+
1772
+ ```python
1773
+ from tools import *
1774
+ ```
1775
+
1776
+ Your IDE understands it as normal Python.
1777
+
1778
+ The preprocessor simply gives that existing syntax an additional build-time meaning:
1779
+
1780
+ > Include this local module's source code in the generated single-file output.
1781
+
1782
+ Therefore:
1783
+
1784
+ > Keep the original multi-file project as the real source code.
1785
+
1786
+ Treat:
1787
+
1788
+ ```text
1789
+ *.flat.py
1790
+ ```
1791
+
1792
+ as:
1793
+
1794
+ > automatically generated build artifacts.
1795
+
1796
+ Do not manually edit `.flat.py`.
1797
+
1798
+ Make changes in the original:
1799
+
1800
+ ```text
1801
+ .py
1802
+ ```
1803
+
1804
+ files, then run the preprocessor again.
1805
+
1806
+ ---
1807
+
1808
+ # 39. Common Command Reference
1809
+
1810
+ Basic build:
1811
+
1812
+ ```zsh
1813
+ splatfold -i main.py
1814
+ ```
1815
+
1816
+ Specify output:
1817
+
1818
+ ```zsh
1819
+ splatfold \
1820
+ -i main.py \
1821
+ -o app.py
1822
+ ```
1823
+
1824
+ Validation only:
1825
+
1826
+ ```zsh
1827
+ splatfold \
1828
+ -i main.py \
1829
+ --check-only
1830
+ ```
1831
+
1832
+ List dependencies:
1833
+
1834
+ ```zsh
1835
+ splatfold \
1836
+ -i main.py \
1837
+ --list-deps
1838
+ ```
1839
+
1840
+ Verbose diagnostics:
1841
+
1842
+ ```zsh
1843
+ splatfold \
1844
+ -i main.py \
1845
+ --check-only \
1846
+ --list-deps \
1847
+ -v
1848
+ ```
1849
+
1850
+ Inline source tracing:
1851
+
1852
+ ```zsh
1853
+ splatfold \
1854
+ -i main.py \
1855
+ --inline-source-map
1856
+ ```
1857
+
1858
+ Source-marker tracing:
1859
+
1860
+ ```zsh
1861
+ splatfold \
1862
+ -i main.py \
1863
+ --source-marker
1864
+ ```
1865
+
1866
+ Clean source-marker build:
1867
+
1868
+ ```zsh
1869
+ splatfold \
1870
+ -i main.py \
1871
+ --source-marker \
1872
+ --no-markers
1873
+ ```
1874
+
1875
+ Clean release build:
1876
+
1877
+ ```zsh
1878
+ splatfold \
1879
+ -i main.py \
1880
+ -o dist/app.py \
1881
+ --no-markers
1882
+ ```
1883
+
1884
+ Strict build:
1885
+
1886
+ ```zsh
1887
+ splatfold \
1888
+ -i main.py \
1889
+ --strict
1890
+ ```
1891
+
1892
+ Additional module search paths:
1893
+
1894
+ ```zsh
1895
+ splatfold \
1896
+ -i main.py \
1897
+ -I src \
1898
+ -I shared
1899
+ ```
1900
+
1901
+ Display help:
1902
+
1903
+ ```zsh
1904
+ splatfold --help
1905
+ ```
1906
+
1907
+ Display version:
1908
+
1909
+ ```zsh
1910
+ splatfold --version
1911
+ ```