@suss/adapter-python 0.33.1 → 0.34.0

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.
package/dist/index.d.cts CHANGED
@@ -1,9 +1,18 @@
1
- import { TypeShape, WrapperReference, BehavioralSummary } from '@suss/behavioral-ir';
1
+ import { RequestSpellingMetadata, TypeShape, WrapperReference, BehavioralSummary } from '@suss/behavioral-ir';
2
2
  import { Tree, Node } from 'web-tree-sitter';
3
3
  export { Node as PyNode, Tree as PyTree } from 'web-tree-sitter';
4
4
  import { Database } from '@suss/datalog';
5
- import { RawCodeStructure, ExtractorOptions, TimingReport, ExtractionReport, CacheDiagnostic } from '@suss/extractor';
6
- import { WhyExplained } from '@suss/resolution';
5
+ import { UnwrapsByName, WhyExplained } from '@suss/resolution';
6
+ import { RawCodeStructure, ExtractorOptions, TimingReport, ExtractionReport, CacheDiagnostic, DeclaredModule, KeptParses } from '@suss/extractor';
7
+
8
+ /**
9
+ * Loads the Python grammar into web-tree-sitter and parses a file.
10
+ *
11
+ * tree-sitter runs as WASM, so the adapter needs no native build step.
12
+ * The rest of the adapter reaches the parser only through this module
13
+ * and the node helpers in the `ast` module, so a different Python grammar would
14
+ * be a change here alone.
15
+ */
7
16
 
8
17
  /**
9
18
  * A new `Parser` for every call, so that parses running at the same time do
@@ -50,7 +59,7 @@ type ModuleResolution = {
50
59
  reason: "external" | "ambiguous" | "outsideRoots";
51
60
  };
52
61
  interface ModuleResolverOptions {
53
- /** The directories an absolute dotted import is resolved against. We cannot see `sys.path` order, since that only exists at runtime. */
62
+ /** The directories an absolute dotted import is resolved against. Their order does not matter, because the order of `sys.path` exists only at run time. */
54
63
  roots: string[];
55
64
  }
56
65
  declare function resolveAbsoluteModule(dotted: string, options: ModuleResolverOptions): ModuleResolution;
@@ -64,52 +73,50 @@ declare function resolveRelativeModule(importingFile: string, spec: RelativeModu
64
73
  declare function resolveModule(importingFile: string, spec: RelativeModuleSpec, options: ModuleResolverOptions): ModuleResolution;
65
74
 
66
75
  /**
67
- * The Python adapter's own pattern-pack contract.
76
+ * The contract a Python pack is written against.
68
77
  *
69
- * This is deliberately not the TypeScript adapter's `PatternPack`. That type
70
- * dispatches its discovery variants through ts-morph-specific handlers, and
71
- * Python's two route shapes have no exact match in its union anyway. Match
72
- * shapes stay per-language until a second implementation shows what is actually
73
- * shared, and this is the per-language one for Python.
78
+ * It is separate from the TypeScript adapter's `PatternPack` on purpose. That
79
+ * type dispatches its discovery variants through handlers built on ts-morph,
80
+ * and Python's two kinds of route have no exact match in its union. Each
81
+ * language keeps its own match patterns until a second implementation shows
82
+ * which parts the two have in common.
74
83
  *
75
- * A pack is still plain data, following the same rule the TypeScript packs
76
- * follow: it describes what a library defines, never anything a project chose.
84
+ * A pack is plain data under the same rule the TypeScript packs follow: it
85
+ * describes what a library defines, and nothing a project chose.
77
86
  */
87
+
78
88
  interface PythonPack {
79
89
  name: string;
80
90
  /**
81
- * Pack version stamp, which feeds the cache invalidation key. Bump on
82
- * any change that affects discovered units or extracted summaries.
83
- * The CLI folds a hash of the loaded pack file and its config into
84
- * this stamp on top, so a pack run through the CLI invalidates on an
85
- * edit whether or not it declares a version of its own.
91
+ * Part of the cache key. Bump it on any change that affects the units a
92
+ * pack discovers or the summaries it produces. The CLI also adds a hash
93
+ * of the loaded pack file and its config to the key, so a pack run
94
+ * through the CLI invalidates the cache on an edit whether or not it
95
+ * declares a version.
86
96
  */
87
97
  version?: string;
88
98
  /**
89
- * Files under the project this pack reads that are not among the
90
- * `.py` files a run walks, given the files the run is about to walk.
91
- * Their content feeds the same cache key the pack's own config does,
92
- * so an edit to one of them re-extracts instead of handing back the
93
- * previous answer.
99
+ * Given the `.py` files a run is about to walk, returns the other files
100
+ * in the project this pack reads. Their content goes into the cache key
101
+ * along with the pack's config, so editing one of them re-extracts
102
+ * instead of returning the cached summaries.
94
103
  */
95
104
  discoveryInputs?: (files: readonly string[]) => string[];
96
105
  /** Wire protocol for the produced boundary bindings, e.g. "http". */
97
106
  protocol: string;
98
107
  discovery: PythonDiscoveryPattern[];
99
108
  /**
100
- * Modules the project itself supplies, the wrappers a person names
101
- * when configuring the pack. The library's own module is not one of
102
- * these: it lives outside the project and would never resolve. The
103
- * adapter checks each of these against the project's roots, because
104
- * a wrapper that resolves to nothing does not match any decorator
105
- * and never says why (#188).
109
+ * Wrapper modules around the library that a person lists when
110
+ * configuring the pack. The library's own module does not go here. The adapter
111
+ * reports each of these that no file in the run imports, since
112
+ * otherwise it would match no decorator and nothing would say why (#188).
106
113
  */
107
114
  projectModules?: string[];
108
115
  /** The callables the library gives a project for making a request. */
109
116
  clients?: PyClientCall[];
110
- /** What the library's own database queries look like. The README says how one is matched. */
117
+ /** What the library's own database queries look like. DESIGN.md says how one is matched. */
111
118
  storage?: StoragePattern[];
112
- /** Which of the library's calls give back one of a model class. The README says what a chain of them composes into. */
119
+ /** Which of the library's calls give back one of a model class. DESIGN.md says what a chain of them composes into. */
113
120
  models?: PyModelQueries[];
114
121
  /** Which of the library's classes return themselves from `__enter__`, so a `with` block gets the object itself. */
115
122
  contextManagers?: PyContextManager[];
@@ -117,12 +124,93 @@ interface PythonPack {
117
124
  rawSql?: RawSqlPattern[];
118
125
  /** The client objects the library hands a project, and the calls on one that reach the database. */
119
126
  sqlClients?: SqlClientPattern[];
127
+ /** Functions the library exports that hand back the argument at `argument`, such as a decorator that returns the function it was given. */
128
+ transparentWrappers?: UnwrapsByName[];
129
+ /**
130
+ * The objects the library puts the request on for a route to read,
131
+ * such as Flask's module-level `request`. A route is handed no
132
+ * parameter for them, so without this a value read off one is not
133
+ * known to come from the request.
134
+ */
135
+ requestObjects?: RequestObject[];
136
+ /** Where a route reads each part of the request, recorded on every route the pack finds. */
137
+ requestSpelling?: RequestSpellingMetadata;
138
+ /** How the library's test runner collects tests, so a PRD scenario can say which test covers it. */
139
+ tests?: PyTestCases[];
140
+ }
141
+ /**
142
+ * How a test runner finds tests and what it runs around each one. Every
143
+ * name here is one the runner itself defines or documents; a project's
144
+ * own conventions come through `files`, which lists the files to read.
145
+ * DESIGN.md says how the adapter reads each field.
146
+ */
147
+ interface PyTestCases {
148
+ /** Base-name patterns of the files the runner collects, `*` matching any run of characters: `test_*.py`. */
149
+ filePatterns: string[];
150
+ /** What a test function or method name starts with: `test`. */
151
+ functionPrefix: string;
152
+ /** What a class of tests is named starting with: `Test`. */
153
+ classPrefix: string;
154
+ /** Base classes whose subclasses contain tests whatever the class is called, as dotted names: `unittest.TestCase`. */
155
+ caseBaseClasses: string[];
156
+ /** The functions and methods the runner calls before a test, by where they are written. */
157
+ setUp: PyTestSetUp;
158
+ /** Markers whose string arguments are the fixtures every test under them runs through, as dotted names: `pytest.mark.usefixtures`. */
159
+ usesFixtureMarkers: string[];
160
+ /** Decorators that declare a fixture, as dotted names: `pytest.fixture`. */
161
+ fixtureDecorators: string[];
162
+ /** The keyword on a fixture decorator that renames the fixture: `name`. */
163
+ fixtureNameKeyword: string;
164
+ /** The keyword on a fixture decorator that runs it before every test in its scope: `autouse`. */
165
+ autouseKeyword: string;
166
+ /** Files whose fixtures apply to every test under their directory: `conftest.py`. */
167
+ sharedFixtureFiles: string[];
168
+ /** Decorators that mark a test so it does not run, or runs expecting to fail, as dotted names: `pytest.mark.skip`. */
169
+ skipDecorators: string[];
170
+ /** The module or class variable the runner reads markers from for every test under it: `pytestmark`. */
171
+ markerVariable?: string;
172
+ /** Parameters the runner fills with something other than a fixture, which are never looked up: `self`, `request`. */
173
+ reservedParameters: string[];
174
+ /** How a test replaces something before it runs. */
175
+ mocks: PyTestMocks;
176
+ /** Read only these files, matched on whole path segments from the end. Left out, every collected file is read. */
177
+ files?: string[];
120
178
  }
121
179
  /**
122
- * A call talks to the database when the method behind it says it returns one
123
- * of the library's query types. Matching on the return rather than on the
124
- * import is what reads a project base class that wraps the library, which is
125
- * how the measured corpus writes every one of its queries.
180
+ * The setup a runner calls before a test, listed in the order it calls
181
+ * them. A method is looked for on the class and then on the project
182
+ * classes it extends, since the setup is often written on a base class.
183
+ */
184
+ interface PyTestSetUp {
185
+ /** Functions in the test's module that run before any test in it: `setup_module`. */
186
+ module: string[];
187
+ /** Functions in the test's module that run before each test written as a function: `setup_function`. */
188
+ function: string[];
189
+ /** Methods of a class of tests that run before each test in it: `setup_class`, `setup_method`. */
190
+ testClass: string[];
191
+ /** Methods of a `caseBaseClasses` subclass that run before each test in it: `setUpClass`, `setUp`. */
192
+ caseClass: string[];
193
+ }
194
+ /**
195
+ * The calls that replace a module or a member for the length of a test.
196
+ * A patcher called with a dotted string replaces the object at that path,
197
+ * `patch("orders.service.cancel")`. Called with an object and a member
198
+ * name, it replaces that member, `patch.object(OrderService, "cancel")`.
199
+ */
200
+ interface PyTestMocks {
201
+ /** Patchers imported from a library, as dotted names: `unittest.mock.patch`, `unittest.mock.patch.object`. */
202
+ patchers: string[];
203
+ /** Patchers a fixture hands the test, by the fixture's parameter name and the methods called on it. */
204
+ fixturePatchers: {
205
+ fixture: string;
206
+ methods: string[];
207
+ }[];
208
+ }
209
+ /**
210
+ * A call talks to the database when the method behind it declares that it
211
+ * returns one of the library's query types. Matching on the return type finds
212
+ * queries made through a project base class that wraps the library, where a
213
+ * match on the import would find none.
126
214
  */
127
215
  interface StoragePattern {
128
216
  /** The module a query type is imported from, `sqlalchemy.orm` for a Session. */
@@ -132,39 +220,37 @@ interface StoragePattern {
132
220
  /** Chain-ending methods that change what is stored. Anything else reads. */
133
221
  writes: string[];
134
222
  /**
135
- * Methods on a query type that touch no rows of their own: one that runs
136
- * a statement built elsewhere, `session.execute(stmt)`, whose own chain
137
- * already records the work, and one that manages the session, `close`.
138
- * A call to one of these records nothing.
223
+ * Methods on a query type that touch no rows of their own, and a call to
224
+ * one records nothing. `session.execute(stmt)` runs a statement whose own
225
+ * chain already records the work, and `close` manages the session.
139
226
  */
140
227
  recordsNothing?: string[];
141
228
  /**
142
- * Methods whose keywords supply column values rather than pick rows,
143
- * `values` in `update(User).where(id=1).values(name="x")`. Their
144
- * keywords are reported as fields; every other call's keywords are the
145
- * selector.
229
+ * Methods whose keywords supply column values instead of picking rows,
230
+ * `values` in `update(User).where(id=1).values(name="x")`. Their keywords
231
+ * are reported as fields, and every other call's keywords as the selector.
146
232
  */
147
233
  valueMethods?: string[];
148
234
  /**
149
- * Functions the library exports that start a query on their own, for the
150
- * case where a call site imports one rather than reaching it through a
151
- * project class. `select(...)` in SQLAlchemy 2.0 is one.
235
+ * Functions the library exports that start a query on their own, such as
236
+ * SQLAlchemy 2.0's `select(...)`. A call site imports one directly instead
237
+ * of reaching it through a project class.
152
238
  */
153
239
  queryFunctions?: string[];
154
240
  /** Which database the library is talking to, for the boundary binding. */
155
241
  storageSystem: "postgresql" | "mysql" | "sqlite";
156
242
  }
157
243
  /**
158
- * What a library gives back when a call is passed one of a project's
159
- * model classes, so a method read off the result runs the one that
160
- * class declares. SQLAlchemy and SQLModel take the class as an argument,
161
- * `session.get(User, id)` and `select(User)`, rather than as the
162
- * receiver a Rails finder is called on.
244
+ * Which of a library's calls give back an instance of the project model
245
+ * class they were passed, so a method called on the result resolves to the
246
+ * one that class declares. SQLAlchemy and SQLModel take the class as an
247
+ * argument, as in `session.get(User, id)` and `select(User)`, where a Rails
248
+ * finder is called on the class itself.
163
249
  */
164
250
  interface PyModelQueries {
165
251
  /**
166
- * The names a model's ancestry arrives at: a base class the library
167
- * exports, `DeclarativeBase`, or the function that builds one,
252
+ * The names a model's base classes lead back to: a base class the
253
+ * library exports, `DeclarativeBase`, or the function that builds one,
168
254
  * `declarative_base`.
169
255
  */
170
256
  baseNames: string[];
@@ -172,16 +258,16 @@ interface PyModelQueries {
172
258
  givesBack: string[];
173
259
  /** Methods that take the model class and give back one of it, with the position it is written at. */
174
260
  entryMethods: PyModelEntryMethod[];
175
- /** Functions the library exports that do the same, called on their own rather than read off a session. */
261
+ /** Functions the library exports that do the same, called on their own instead of on a session. */
176
262
  entryFunctions: PyModelEntryFunction[];
177
263
  /** The library's own constructors for a field that reaches another model. */
178
264
  relationships?: PyRelationshipConstructor[];
179
265
  }
180
266
  /**
181
- * `items: list[Item] = Relationship(...)`: the callable a model's field
182
- * is given to say it reaches another model, and where it comes from. A
183
- * project function spelled the same way is somebody else's, so the
184
- * import is what settles a match.
267
+ * The callable a model field is assigned from to say it points at another
268
+ * model, as in `items: list[Item] = Relationship(...)`, and where it is
269
+ * imported from. A match needs the import, so a project function with the
270
+ * same name does not count.
185
271
  */
186
272
  interface PyRelationshipConstructor {
187
273
  /** The module it is imported from, `sqlmodel` or `sqlalchemy.orm`. */
@@ -202,9 +288,9 @@ interface PyModelEntryFunction {
202
288
  }
203
289
  /**
204
290
  * The function a library gives a project for handing the database a
205
- * statement written as SQL, and where it comes from. SQLAlchemy exports
206
- * `text` from `sqlalchemy`. A local function of the same name is
207
- * somebody else's, so the import is what settles a match.
291
+ * statement written as SQL, and where it is imported from. SQLAlchemy
292
+ * exports `text` from `sqlalchemy`. A match needs the import, so a local
293
+ * function with the same name does not count.
208
294
  */
209
295
  interface RawSqlPattern {
210
296
  /** The module the function is imported from. */
@@ -216,13 +302,13 @@ interface RawSqlPattern {
216
302
  }
217
303
  /**
218
304
  * A client object a library hands a project, and the calls on one that
219
- * reach the database. `RawSqlPattern` matches a function the file
220
- * imported; this matches a method on a value whose class came out of
221
- * the library, which is how a client built in one module and called in
222
- * another still reads.
305
+ * reach the database. `RawSqlPattern` matches a function the file imported.
306
+ * This matches a method called on a value whose class comes from the
307
+ * library, so a client built in one module and called in another still
308
+ * matches.
223
309
  *
224
- * Every field here is the library's own: the module, the class names,
225
- * the method names, and where each method takes what it is given.
310
+ * Every field describes the library: the module, the class and method
311
+ * names, and where each method takes its input.
226
312
  */
227
313
  interface SqlClientPattern {
228
314
  /** The module the client class comes from, `google.cloud.bigquery`. */
@@ -231,9 +317,9 @@ interface SqlClientPattern {
231
317
  clientTypes: string[];
232
318
  /** The methods that hand the database a statement, and where each takes it. */
233
319
  statements?: SqlStatementCall[];
234
- /** The methods that say which table rather than writing SQL, and what each does to it. */
320
+ /** The methods that take a table name instead of SQL, and what each does to the table. */
235
321
  tables?: SqlTableCall[];
236
- /** The methods that hand back one of another library class, so a chain off one reaches its calls. */
322
+ /** The methods that return an instance of another of the library's classes, so a chain can continue through it. */
237
323
  handsBack?: SqlClientHandoff[];
238
324
  /** Which database the client is talking to, for the boundary binding. */
239
325
  storageSystem: string;
@@ -252,20 +338,20 @@ interface SqlStatementCall extends SqlCallArgument {
252
338
  /** The method name, `query`. */
253
339
  method: string;
254
340
  /**
255
- * The keys down to the statement when it travels inside a dictionary
256
- * rather than as the argument itself, the `["query", "query"]` of
341
+ * The keys to follow when the statement is passed inside a dictionary
342
+ * instead of as the argument itself: `["query", "query"]` for
257
343
  * `insert_job(configuration={"query": {"query": sql}})`.
258
344
  */
259
345
  path?: string[];
260
346
  }
261
- /** One method that says which table it works on, and what it does to it. */
347
+ /** One method that takes the name of the table it works on, and what it does to that table. */
262
348
  interface SqlTableCall extends SqlCallArgument {
263
349
  /** The method name, `get_table`. */
264
350
  method: string;
265
351
  /** Whether the call changes what is stored. */
266
352
  kind: "read" | "write";
267
353
  }
268
- /** One method that hands back an object of another of the library's classes. */
354
+ /** One method that returns an instance of another of the library's classes. */
269
355
  interface SqlClientHandoff {
270
356
  /** The method name, Airflow's `get_client`. */
271
357
  method: string;
@@ -276,10 +362,10 @@ interface SqlClientHandoff {
276
362
  }
277
363
  type PythonDiscoveryPattern = DecoratedClassRoute | DecoratedFunctionRoute;
278
364
  /**
279
- * A class whose `__enter__` gives back the object it was called on, so
280
- * `with X() as y` makes y the `X()` the block opened. Python
281
- * lets `__enter__` return anything, so only the library that wrote the
282
- * class can say this, and a pack declares it for its own classes alone.
365
+ * Classes whose `__enter__` returns the object it was called on, so
366
+ * `with X() as y` binds `y` to the `X()` the block opened. Python lets
367
+ * `__enter__` return anything, so only the library that wrote the class
368
+ * knows this, and a pack declares it only for its own library's classes.
283
369
  */
284
370
  interface PyContextManager {
285
371
  /** The module the classes come from, `httpx`. */
@@ -289,8 +375,8 @@ interface PyContextManager {
289
375
  }
290
376
  /**
291
377
  * The callables a library gives a project for making a request. A
292
- * function that calls one of them is a client of the boundary that call
293
- * states, and gets a unit bound to its method and path.
378
+ * function that calls one of them becomes a client unit, bound to the
379
+ * method and path that call is made with.
294
380
  */
295
381
  interface PyClientCall {
296
382
  type: "clientCall";
@@ -316,9 +402,9 @@ interface PyClientCall {
316
402
  response?: PyClientResponse;
317
403
  }
318
404
  /**
319
- * The members a library's response object gives a caller. A caller that
320
- * tests one of them is saying which statuses it handles, and the
321
- * checker compares that against what the other side produces.
405
+ * The members a library's response object gives a caller. A test on one of
406
+ * them shows which statuses the caller handles, and the checker compares
407
+ * that against what the other side sends.
322
408
  */
323
409
  interface PyClientResponse {
324
410
  /** Members whose value is the status code: `status_code`. */
@@ -330,62 +416,80 @@ interface PyClientResponse {
330
416
  /** Whether a refused request comes back as a response or raises where it was made. */
331
417
  failureDelivery?: "response" | "exception";
332
418
  }
419
+ /** An object a module exports that a route reads the request off. */
420
+ interface RequestObject {
421
+ module: string;
422
+ name: string;
423
+ }
333
424
  /** Conventions both kinds of route share. Each one describes what the library does, never a project's choice. */
334
425
  interface RouteConventions {
335
- /** How the library spells a path parameter. The README lists the syntaxes we know how to read. */
426
+ /** How the library writes a path parameter. DESIGN.md lists the syntaxes the adapter reads. */
336
427
  pathParamSyntax?: string;
337
428
  /** Set it only when the library itself binds an annotated local class to the request body. */
338
429
  annotatedClassIsRequestBody?: boolean;
339
430
  /**
340
- * Callables the library uses to inject a parameter rather than read it
341
- * off the request. FastAPI's `Depends` and `Security` are these: the
342
- * server supplies the value and the client sends nothing, so a
343
- * parameter defaulted to one of them is no part of the request however
344
- * its annotation reads.
431
+ * Callables the library uses to inject a parameter instead of reading it
432
+ * off the request, such as FastAPI's `Depends` and `Security`. The server
433
+ * supplies the value and the client sends nothing, so a parameter
434
+ * defaulted to one of them is not part of the request, whatever its
435
+ * annotation says.
345
436
  */
346
437
  injectedParameterCallees?: string[];
347
438
  /**
348
- * What the library serves when a composed path ends up with repeated
349
- * slashes in it. Werkzeug serves the merged path and redirects
350
- * the written one, so "merged" is what Flask needs; "kept" is the
351
- * default and is what Starlette does.
439
+ * What the library serves when a composed path has repeated slashes.
440
+ * Werkzeug serves the merged path and redirects the written one, so Flask
441
+ * needs "merged". The default, "kept", matches Starlette.
352
442
  */
353
443
  pathRepeatedSlashes?: PathRepeatedSlashes;
354
- /** The status the library returns for a declared response when the route does not give one. Library-defined. */
444
+ /** The status the library returns for a declared response when the route does not set one. */
355
445
  defaultStatusCode?: number;
356
446
  /**
357
447
  * Set it only when the library reads a status out of the tuple a handler
358
- * returns, as Flask does with `return body, 201`. Without it
359
- * the library default applies whatever the body returns, and a route
360
- * that sets its own status would be reported at the default.
448
+ * returns, as Flask does with `return body, 201`. Without it, every return
449
+ * is reported at the library's default status, including a route that
450
+ * sets its own.
361
451
  */
362
452
  statusFromReturnedTuple?: boolean;
363
453
  /**
364
454
  * The library's own callables that end the request with a status, such
365
- * as FastAPI's `HTTPException` and Flask's `abort`. Declaring them is
366
- * also what makes a `raise` in a route body an outcome of its own: a
367
- * raise the list does not cover comes out as a throw with no status,
368
- * and one it does cover comes out as the response the library sends.
455
+ * as FastAPI's `HTTPException` and Flask's `abort`. A `raise` of one in a
456
+ * route body becomes the response the library sends. A raise of anything
457
+ * the list does not cover becomes a throw with no status.
369
458
  */
370
459
  responseStatusCalls?: PyStatusCall[];
371
460
  /**
372
- * The library's own classes whose instance, when a body returns it, is
373
- * the response the library sends, and where each takes the status:
374
- * Starlette's `JSONResponse(status_code=...)`. A return of one says its
375
- * own status; a return of anything else keeps the declared one.
461
+ * The library's own classes that are sent as the response when a body
462
+ * returns an instance, and where each takes the status, as in Starlette's
463
+ * `JSONResponse(status_code=...)`. Returning one gives that status, and
464
+ * returning anything else keeps the declared one.
376
465
  */
377
466
  responseConstructors?: PyStatusCall[];
378
467
  /** Unset means the library has no router mounting, and a route's decorator path is used as written. */
379
468
  routerComposition?: RouterComposition;
380
- /** The ways the library runs a project's own function around a route. The README lists what each one covers. */
469
+ /**
470
+ * The callables that say which part of the request a parameter is read
471
+ * from, by the callee's name, written as the default or inside
472
+ * `Annotated[...]`: FastAPI's `x_tenant_id: str = Header()` is a header.
473
+ */
474
+ parameterSources?: Record<string, ParameterSource>;
475
+ /** The keyword those callables take a field's name under when it differs from the parameter's, FastAPI's `alias`. */
476
+ parameterAliasKeyword?: string;
477
+ /** The ways the library runs a project's own function around a route. DESIGN.md lists what each one covers. */
381
478
  wrappers?: PyWrapperForm[];
382
479
  }
480
+ /** The part of the request a parameter declared with one callable is read from. */
481
+ interface ParameterSource {
482
+ /** The role the parameter gets, `headers` for FastAPI's `Header`. */
483
+ role: string;
484
+ /** What the library writes in place of `_` in the field's name, `-` for a FastAPI header. */
485
+ underscoresAs?: string;
486
+ }
383
487
  type PyWrapperForm = PyDependencyForm | PyDecoratedWrapperForm;
384
488
  /** Where a wrapper is registered, and which routes the registration reaches. */
385
489
  interface PyWrapperRegistrar {
386
490
  /** The constructor of the object the registration is written on, as the library exports it: `FastAPI`, `APIRouter`. */
387
491
  constructorName: string;
388
- /** Where the constructor is imported from, when that is not the pattern's own `importModule`: `flask` for the app a flask-restx API is served by. */
492
+ /** Where the constructor is imported from, when that differs from the pattern's own `importModule`: `flask` for the app that serves a flask-restx API. */
389
493
  importModule?: string[];
390
494
  /**
391
495
  * `everyRoute` for the app, whose registration reaches every route of
@@ -395,13 +499,12 @@ interface PyWrapperRegistrar {
395
499
  covers: "everyRoute" | "ownRoutes";
396
500
  }
397
501
  /**
398
- * The library calls a project function before the handler, given as an
399
- * argument to one of its own callables: FastAPI's `Depends(get_user)`.
400
- * One may be written as a parameter default or inside `Annotated[...]`
401
- * on the route, or in a list under `keyword` on the route decorator or
402
- * on one of the registrars. The function runs before the handler and
403
- * ends the request by raising, so it is a wrapper whose every return
404
- * hands on.
502
+ * A project function the library calls before the handler, passed to one
503
+ * of the library's callables, as in FastAPI's `Depends(get_user)`. It can
504
+ * be written as a parameter default, inside `Annotated[...]` on the route,
505
+ * or in a list under `keyword` on the route decorator or a registrar. It
506
+ * ends a request only by raising, so every return passes control on to
507
+ * the route.
405
508
  */
406
509
  interface PyDependencyForm {
407
510
  type: "dependency";
@@ -412,11 +515,10 @@ interface PyDependencyForm {
412
515
  registrars: PyWrapperRegistrar[];
413
516
  }
414
517
  /**
415
- * A project function decorated with a method on the app or a router:
416
- * `@app.middleware("http")`, `@app.exception_handler(ValueError)`,
417
- * `@app.before_request`. What the decorated function's returns mean
418
- * depends on which of the three fields below is set; with none set,
419
- * every return hands the request on.
518
+ * A project function decorated with a method on the app or a router, such
519
+ * as `@app.middleware("http")` or `@app.before_request`. Which of the three
520
+ * optional fields below is set decides what the function's returns mean.
521
+ * With none set, every return passes the request on.
420
522
  */
421
523
  interface PyDecoratedWrapperForm {
422
524
  type: "decoratedWrapper";
@@ -430,17 +532,17 @@ interface PyDecoratedWrapperForm {
430
532
  */
431
533
  continuationParam?: number;
432
534
  /**
433
- * Set when a value returned ends the request and only a bare return
434
- * hands on. Flask's `before_request` works this way.
535
+ * Set when returning a value ends the request and only a bare return
536
+ * passes it on. Flask's `before_request` works this way.
435
537
  */
436
538
  returnedValueResponds?: boolean;
437
- /** The position the library hands the raised exception at. Set on an error handler, which runs only when the handler raised. */
539
+ /** The parameter position the raised exception is passed at. Set on an exception handler, which runs only when the route raised. */
438
540
  throwParam?: number;
439
541
  }
440
542
  /**
441
- * One callable that ends the request, and where it takes the status.
442
- * A call may take it either way, so a pattern may state both, and the
443
- * keyword wins where an argument is written both ways.
543
+ * One callable that ends the request, and where it takes the status. A
544
+ * call can pass the status by keyword or by position, so a pattern can
545
+ * declare both. When a call writes both, the keyword is used.
444
546
  */
445
547
  interface PyStatusCall {
446
548
  /** The callee as the file imports it, module and name together, `fastapi.HTTPException`. */
@@ -453,9 +555,9 @@ interface PyStatusCall {
453
555
  defaultStatusCode?: number;
454
556
  }
455
557
  /**
456
- * A class has a decorator whose first string-literal argument is the route path,
457
- * and each method in its body named after an HTTP verb becomes its own unit,
458
- * with the verb taken from the method name.
558
+ * A route written as a class whose decorator takes the route path as its
559
+ * first argument. Each method in the class named after an HTTP verb becomes
560
+ * its own unit, with the verb taken from the method name.
459
561
  */
460
562
  interface DecoratedClassRoute extends RouteConventions {
461
563
  type: "decoratedClassRoute";
@@ -467,10 +569,10 @@ interface DecoratedClassRoute extends RouteConventions {
467
569
  verbMethodNames: Record<string, string>;
468
570
  }
469
571
  /**
470
- * A function has a decorator whose attribute name is the HTTP verb and whose
471
- * first string-literal argument is the route path, the `@app.get(path)`
472
- * convention. The object it hangs on may be imported, or built one hop away by
473
- * a call to something imported.
572
+ * A route written as a function decorated with `@app.get(path)`: the
573
+ * decorator's attribute name is the HTTP verb and its first argument is the
574
+ * route path. The object the decorator is called on can be imported, or
575
+ * built by a call to something imported.
474
576
  */
475
577
  interface DecoratedFunctionRoute extends RouteConventions {
476
578
  type: "decoratedFunctionRoute";
@@ -482,12 +584,12 @@ interface DecoratedFunctionRoute extends RouteConventions {
482
584
  statusCodeKeyword?: string;
483
585
  }
484
586
  /**
485
- * What a library calls the pieces of router mounting, so a route's served path
486
- * can be built from the literal prefixes written along the way: the router
487
- * constructor's own, the one at the call that mounts it, and, where the pack
488
- * says so, the prefix on the object the mount is called on. What each spelling
489
- * of a prefix means, and what makes a composition abstain, is the grid in the
490
- * adapter's README.
587
+ * The names a library gives the parts of router mounting, so a route's
588
+ * served path can be built from the prefixes written along the way: the
589
+ * router constructor's, the one on the call that mounts it, and, where the
590
+ * pack declares it, the one on the object the mount is called on. DESIGN.md
591
+ * has the tables for what each way of writing a prefix means and when a
592
+ * route abstains.
491
593
  */
492
594
  interface RouterComposition {
493
595
  /** Constructor whose call builds a mountable router, FastAPI's `APIRouter`. */
@@ -495,37 +597,36 @@ interface RouterComposition {
495
597
  /** Method that mounts a router onto the app, FastAPI's `include_router`. */
496
598
  includeMethodName: string;
497
599
  /**
498
- * What the mount method calls its router parameter, FastAPI's `router`.
499
- * A call that passes the router by keyword rather than by position is
500
- * read through this. Unset reads the first argument only.
600
+ * The mount method's name for its router parameter, FastAPI's `router`,
601
+ * used to read a call that passes the router by keyword. Unset reads the
602
+ * first positional argument only.
501
603
  */
502
604
  routerKeyword?: string;
503
- /** One keyword serves the constructor and the mount alike. A library that spells them apart needs two fields here. */
605
+ /** The prefix keyword, the same at the constructor and the mount. A library that uses two different keywords would need two fields here. */
504
606
  prefixKeyword: string;
505
- /** Default "prefixes". */
607
+ /** Whether a prefix written at the mount goes in front of the constructor's ("prefixes") or replaces it ("replaces"). Default "prefixes". */
506
608
  mountPrefixEffect?: MountPrefixEffect;
507
- /** Set it when the library works out a path for a router that gives no prefix. We cannot work that path out, so such routes abstain. */
609
+ /** Set it when the library makes up a path of its own for a router constructed with no prefix. The adapter cannot work that path out, so such routes abstain. */
508
610
  constructorPrefixRequired?: boolean;
509
- /** Default "unreadable". The same setting covers the constructor and the mount. */
611
+ /** What a prefix written as `""`, `None`, `False` or `0` means: no prefix ("unstated"), or one the route has to abstain on ("unreadable"). Default "unreadable", at the constructor and the mount alike. */
510
612
  noValuePrefix?: NoValuePrefix;
511
- /** Default "kept". */
613
+ /** Whether the library trims trailing slashes off a constructor's prefix. Default "kept". */
512
614
  constructorPrefixTrailingSlash?: PrefixTrailingSlash;
513
615
  /**
514
- * Where the object the mount is called on states a prefix of its
515
- * own, in front of everything the constructor and the mount state.
516
- * Unset means it states none, which is FastAPI's behavior: an app
517
- * serves a mounted router exactly where the two prefixes put it.
616
+ * Where the object the mount is called on states a prefix of its own,
617
+ * in front of everything the constructor and the mount state. Unset
618
+ * means it states none. That matches FastAPI, where an app serves a
619
+ * mounted router exactly where the two prefixes put it.
518
620
  */
519
621
  mountObjectPrefix?: MountObjectPrefix;
520
622
  }
521
623
  /**
522
- * The prefix the object a mount is called on states, and where it is
523
- * written. flask-restx needs both halves: `Api(prefix=...)` states one
524
- * on the object itself, and the Flask blueprint the `Api` was built
525
- * from states another with `Blueprint(name, __name__,
526
- * url_prefix=...)`. The library serves a route under the blueprint's
527
- * prefix, then the `Api`'s, then whatever the namespace and the route
528
- * say.
624
+ * Where the object a mount is called on states its prefix. flask-restx
625
+ * needs both fields: `Api(prefix=...)` states one on the object itself,
626
+ * and the Flask blueprint the `Api` was built from states another with
627
+ * `Blueprint(name, __name__, url_prefix=...)`. A route is served under
628
+ * the blueprint's prefix, then the `Api`'s, then the namespace's path and
629
+ * the route's own.
529
630
  */
530
631
  interface MountObjectPrefix {
531
632
  /** Keyword stating a prefix on the mount object's own construction (flask-restx's `Api(prefix=...)`). */
@@ -534,11 +635,10 @@ interface MountObjectPrefix {
534
635
  carrier?: MountObjectCarrier;
535
636
  }
536
637
  /**
537
- * An object handed to the mount object's constructor, one hop further
538
- * out, with a prefix of its own (the Flask blueprint behind an
539
- * `Api`). Naming it here is what lets the adapter tell that object
540
- * apart from the plain app that appears in the same argument position and
541
- * has no prefix at all.
638
+ * An object passed to the mount object's constructor that has a prefix of
639
+ * its own, such as the Flask blueprint an `Api` is built from. Declaring
640
+ * its constructor lets the adapter tell it apart from the plain app, which
641
+ * is passed in the same argument position and has no prefix.
542
642
  */
543
643
  interface MountObjectCarrier {
544
644
  /** Modules the carrier's constructor is imported from (Flask's `flask`). */
@@ -547,32 +647,28 @@ interface MountObjectCarrier {
547
647
  constructorName: string;
548
648
  /** Position of the carrier among the mount object's constructor arguments. */
549
649
  argumentIndex: number;
550
- /** Keyword stating the carrier's prefix, at its construction and at its registration alike (Flask's `url_prefix`). */
650
+ /** The keyword for the carrier's prefix, at its construction and its registration alike (Flask's `url_prefix`). */
551
651
  prefixKeyword: string;
552
652
  /**
553
- * Method handing the carrier to an already-built mount object
554
- * (flask-restx's `init_app`, the application-factory spelling of
555
- * `Api(blueprint)`). Unset means the constructor argument is the
556
- * only way in.
653
+ * Method that passes the carrier to a mount object already built, as
654
+ * flask-restx's `init_app` does in an application factory in place of
655
+ * `Api(blueprint)`. Unset means the constructor argument is the only
656
+ * way to pass one.
557
657
  */
558
658
  handoffMethodName?: string;
559
659
  /**
560
- * Method registering the carrier somewhere else (Flask's
561
- * `register_blueprint`). The adapter reads it only to abstain: a
562
- * registration restating the prefix, putting the carrier inside
563
- * another carrier, or happening twice moves the served path
564
- * somewhere the carrier's own construction no longer says.
660
+ * Method that registers the carrier somewhere else (Flask's
661
+ * `register_blueprint`). The adapter reads it only to decide when to
662
+ * abstain. A registration that restates the prefix, registers the
663
+ * carrier inside another carrier, or registers it twice serves the
664
+ * routes somewhere other than where the carrier's construction says.
565
665
  */
566
666
  registerMethodName: string;
567
667
  }
568
668
  type MountPrefixEffect = "prefixes" | "replaces";
569
669
  type PrefixTrailingSlash = "kept" | "trimmed";
570
670
  type NoValuePrefix = "unstated" | "unreadable";
571
- /**
572
- * Werkzeug serves the merged path and redirects the written one, so Flask
573
- * needs "merged". Starlette leaves the path as composed, so FastAPI keeps
574
- * the default.
575
- */
671
+ /** Werkzeug merges repeated slashes, so Flask needs "merged". Starlette does not, so FastAPI keeps "kept". */
576
672
  type PathRepeatedSlashes = "kept" | "merged";
577
673
 
578
674
  /**
@@ -581,11 +677,11 @@ type PathRepeatedSlashes = "kept" | "merged";
581
677
  * whether that was an import, an assignment, a def, a parameter, or a `global`
582
678
  * or `nonlocal` redirect to another scope.
583
679
  *
584
- * It never has to be complete, because "I could not resolve this name" is a
585
- * legal answer everywhere a name is read. What it has to avoid is being wrong.
586
- * So it only binds names written directly in a body's own statement list. A
587
- * definition or import nested inside an `if`, `try`, or `with` block is not
588
- * found, and a read of it comes back unresolved rather than as a guess.
680
+ * Every reader of a name accepts an unresolved answer, so the binder can
681
+ * leave names out, and a wrong binding is what it avoids. It binds names
682
+ * written directly in a body's statement list, plus the statements of a
683
+ * `with` body. A definition or import nested inside an `if` or `try` is
684
+ * left out, and a read of it comes back unresolved instead of guessed.
589
685
  */
590
686
 
591
687
  type ScopeKind = "module" | "class" | "function";
@@ -625,7 +721,7 @@ type Binding = {
625
721
  } | {
626
722
  kind: "parameter";
627
723
  }
628
- /** The right-hand side, when there is one, so we can trace a decorator's base object one hop back to whatever constructed it. */
724
+ /** The right-hand side, when there is one, so a reader can follow a name one hop back to what constructed it. */
629
725
  | {
630
726
  kind: "assignment";
631
727
  value: Node | null;
@@ -642,7 +738,7 @@ interface ModuleBinding {
642
738
  moduleScope: Scope;
643
739
  /** The scope a class_definition, a function_definition, or the module node opens, keyed by that node's id. */
644
740
  scopeFor: Map<number, Scope>;
645
- /** The modules a `from X import *` pulls in. We cannot list what a wildcard brings in, so nothing expands them here. */
741
+ /** The modules a `from X import *` pulls in. The binder cannot list what a wildcard brings in, so nothing expands them here. */
646
742
  openImports: string[];
647
743
  }
648
744
  declare function bindModule(root: Node): ModuleBinding;
@@ -654,21 +750,35 @@ declare function bindModule(root: Node): ModuleBinding;
654
750
  */
655
751
  declare function resolveName(scope: Scope, name: string): Binding | null;
656
752
 
753
+ /**
754
+ * Works out the prefix a route inherits from the routers it is mounted
755
+ * through, ahead of discovery. With `router = APIRouter(prefix="/items")`
756
+ * and `app.include_router(router, prefix="/api")`, a route on `router` is
757
+ * served under `/api/items`, a path its own file never states. A chain of
758
+ * routers mounted on routers adds a pair of prefixes per hop.
759
+ *
760
+ * Every prefix goes through `readPrefixKeyword` and comes back stated,
761
+ * unstated or unreadable. What each spelling means differs by library, so
762
+ * the pack declares it, and DESIGN.md has the tables. When a path cannot be
763
+ * settled, the route abstains: discovery keeps its name and gives it no
764
+ * path, so it pairs with nothing instead of with a guessed path.
765
+ */
766
+
657
767
  /** One file, already parsed and bound. `buildRouterIndex` takes a project as a list of these. */
658
768
  interface BoundPythonFile {
659
- /** The absolute path, which is what module resolution joins on. */
769
+ /** The absolute path. Module resolution matches files on it. */
660
770
  file: string;
661
- /** The path a gap refers to this file by, which a reader has to be able to open. */
771
+ /** The path a gap reason prints for this file, so a reader can open it. */
662
772
  displayPath: string;
663
773
  root: Node;
664
774
  module: ModuleBinding;
665
775
  }
666
776
  /**
667
- * What the object a decorator hangs on turns out to be. `notRouter` covers the
668
- * app itself and anything the index never saw constructed, so the decorator's
669
- * own path stands as written. `composed` gives the prefix to put in front of
670
- * that path. An `abstain` reason is written to follow "the router this route is
671
- * declared on ...".
777
+ * What the object a route decorator is called on turns out to be. `notRouter`
778
+ * covers the app itself and anything the index never saw constructed, and the
779
+ * decorator's own path is used as written. `composed` gives the prefix to put
780
+ * in front of that path. An `abstain` reason is worded to follow "the router
781
+ * this route is declared on ...".
672
782
  */
673
783
  type RoutePrefixResolution = {
674
784
  kind: "notRouter";
@@ -716,12 +826,14 @@ declare function buildRouterIndex(files: BoundPythonFile[], packs: PythonPack[],
716
826
  interface ImportedDefinition {
717
827
  /** The class_definition, or the value an assignment gave the name. */
718
828
  node: Node;
829
+ /** The file the definition is written in. */
830
+ file: string;
719
831
  /** The module scope of the file the definition is written in. */
720
832
  moduleScope: Scope;
721
833
  /** The scopes of that file, keyed by the node that opens each one. */
722
834
  scopeFor: Map<number, Scope>;
723
835
  }
724
- /** `scope` is where the name is read, which is what keys the import in the facts. */
836
+ /** `scope` is where the name is read. The facts key an import on the file that reads it. */
725
837
  type ImportedDefinitionLookup = (scope: Scope, name: string) => ImportedDefinition | null;
726
838
 
727
839
  /**
@@ -733,11 +845,11 @@ type ImportedDefinitionLookup = (scope: Scope, name: string) => ImportedDefiniti
733
845
  */
734
846
 
735
847
  /**
736
- * `definitions` stores each converted class shape once, however many annotations
737
- * mention it. `scopeMaps` is here so a name written inside a referenced class's
738
- * body resolves too, not only one written at the annotation's use site; a class
739
- * read from another file brings that file's scopes along. `importedDefinition`
740
- * is absent when a caller reads one file on its own, and an imported name is
848
+ * `definitions` stores each converted class shape once, however many
849
+ * annotations mention it. `scopeMaps` lets a name written inside a
850
+ * referenced class's body resolve as well as one at the annotation, and a
851
+ * class read from another file adds that file's scopes. `importedDefinition`
852
+ * is null when a caller reads one file on its own, and an imported name is
741
853
  * then a ref by name and nothing more.
742
854
  */
743
855
  interface AnnotationContext {
@@ -747,7 +859,7 @@ interface AnnotationContext {
747
859
  /** The alias values being expanded, so `A = B` and `B = A` end rather than recurse. */
748
860
  expanding: Set<number>;
749
861
  }
750
- /** `scope` is where the annotation is written, which is how we tell a project-local class from an external name. */
862
+ /** `scope` is where the annotation is written. Resolving the name there tells a project class apart from an external name. */
751
863
  declare function annotationToShape(typeNode: Node, scope: Scope, ctx: AnnotationContext): TypeShape;
752
864
  /** Exported so a decorator keyword that gives a class name reads the same way as that name written in annotation position. */
753
865
  declare function shapeFromName(name: string, scope: Scope, ctx: AnnotationContext): TypeShape;
@@ -757,7 +869,20 @@ interface Range {
757
869
  end: number;
758
870
  }
759
871
 
760
- /** An argument as written, plus its node so a reader can evaluate what it comes down to. */
872
+ /**
873
+ * Works out which library a decorator comes from, for discovery.
874
+ *
875
+ * `@app.route("/x")` is two steps: the call that builds the decorator, and
876
+ * applying the result to the function. Discovery needs only the first, so
877
+ * `classifyDecorator` reads which name the callee resolved to and which
878
+ * module that name came from, and stops there.
879
+ *
880
+ * A decorator re-exported through a project wrapper is handled the same
881
+ * way the TypeScript packs handle one: the name is classified as an import
882
+ * of some module, and the pack lists every module it accepts.
883
+ */
884
+
885
+ /** An argument as written, plus its node so a reader can evaluate it. */
761
886
  type DecoratorArg = DecoratorArgShape & {
762
887
  readonly node: Node;
763
888
  };
@@ -790,15 +915,15 @@ type DecoratorArgShape = {
790
915
  kind: "other";
791
916
  };
792
917
  interface DecoratorClassification {
793
- /** The name as its source module exports it, not the local alias or attribute path it was written under. */
918
+ /** The name as its source module exports it, whatever alias or attribute path the file wrote it under. */
794
919
  importedName: string | null;
795
920
  /** The dotted module the name was imported from. It is null whenever `importedName` is null. */
796
921
  module: string | null;
797
- /** The local variable an attribute decorator hangs on, `app` in `@app.get(...)`. */
922
+ /** The local variable an attribute decorator is called on, `app` in `@app.get(...)`. */
798
923
  objectName: string | null;
799
924
  args: DecoratorArg[];
800
925
  keywordArgs: Record<string, DecoratorArg>;
801
- /** Where the decorator is written. Anything we read out of its arguments uses this as its provenance. */
926
+ /** Where the decorator is written. Anything read out of its arguments reports this as its location. */
802
927
  range: Range;
803
928
  /**
804
929
  * The module the decorator's object lives in, when the decorator was read
@@ -807,9 +932,9 @@ interface DecoratorClassification {
807
932
  */
808
933
  objectModule?: ModuleBinding;
809
934
  /**
810
- * The call the rules say built the object this decorator hangs on. A
811
- * router the index never saw under a name is looked up by this call
812
- * instead, and a decorator on `self.router` has no name to look up.
935
+ * The call the rules say built the object this decorator is called on.
936
+ * The router index looks a router up by this call when it never saw the
937
+ * router under a name, as with a decorator on `self.router`.
813
938
  */
814
939
  subjectConstruction?: {
815
940
  key: string;
@@ -818,6 +943,13 @@ interface DecoratorClassification {
818
943
  }
819
944
  declare function classifyDecorator(decoratorNode: Node, module: ModuleBinding, facts?: Database): DecoratorClassification;
820
945
 
946
+ /**
947
+ * Finds the calls in a body that talk to the database, and what each one
948
+ * does. A pack lists its library's query types, and a call chain matches
949
+ * when the method behind it declares that it returns one. DESIGN.md says
950
+ * why the match goes by return type.
951
+ */
952
+
821
953
  /**
822
954
  * What discovery uses for one file. `factsPath` is the path the facts were
823
955
  * keyed under, which is the absolute one, while a summary shows the short one.
@@ -826,7 +958,6 @@ interface StorageLookup {
826
958
  readonly facts: Database;
827
959
  readonly factsPath: string;
828
960
  readonly patterns: readonly StoragePattern[];
829
- readonly definitionAt: (key: string) => Node | undefined;
830
961
  readonly couldMatch: ReadonlySet<string>;
831
962
  /** What a pack says about statements a project writes as SQL itself. */
832
963
  readonly rawSql?: readonly RawSqlPattern[];
@@ -835,15 +966,16 @@ interface StorageLookup {
835
966
  }
836
967
 
837
968
  /**
838
- * What a call's callee is: a function in this run the walk can step
839
- * into, or a reason it cannot.
969
+ * What a call's callee is: a function in this run that the walk can
970
+ * follow, or the reason it cannot.
840
971
  *
841
- * The rules in @suss/resolution decide it. Every language feature that
842
- * moves a value is a hop they already state, so nothing here reads a
843
- * name, an alias, an attribute, or an instance for itself.
972
+ * The rules in `@suss/resolution` settle the callee. They already follow
973
+ * a value through names, aliases, attributes and instances, so nothing
974
+ * here reads any of those itself.
844
975
  *
845
- * What is left is about files rather than values: a module before the
846
- * dot, and a name only a wildcard import could have brought in.
976
+ * This module handles the two cases that depend on files: a module
977
+ * written before the dot, and a name that only a wildcard import could
978
+ * have brought in.
847
979
  */
848
980
 
849
981
  /** A function in this run, and the export path its summary gets. */
@@ -859,16 +991,16 @@ interface ReachedFunction {
859
991
  /**
860
992
  * The project functions a library runs around a route, read from where
861
993
  * they are registered: a dependency on a route's parameter or decorator,
862
- * a dependency list on the app or a router, a function decorated with
863
- * `@app.middleware(...)` or `@app.exception_handler(...)`. Each one
864
- * becomes a unit of its own, and every route it reaches lists it, so the
865
- * extractor can fold its outcomes into the route's. The README says which
866
- * registrations are read and which are not.
994
+ * a dependency list on the app or a router, or a function decorated with
995
+ * `@app.middleware(...)` or `@app.exception_handler(...)`. Each becomes a
996
+ * unit of its own, and every route it reaches lists it, so the extractor
997
+ * can fold its outcomes into the route's. DESIGN.md, under "What runs
998
+ * around a route", lists which registrations are read.
867
999
  *
868
1000
  * A registration on the app reaches every route of the pack in the run,
869
- * since an app is one per run for every pack that has one. A registration
870
- * on a router reaches the routes decorated on that same router object,
871
- * and nothing mounted onto it.
1001
+ * because the run treats a pack's app as the only one. A registration on
1002
+ * a router reaches the routes decorated on that same router object, and
1003
+ * nothing mounted onto it.
872
1004
  */
873
1005
 
874
1006
  interface WrapperIndexOptions {
@@ -894,6 +1026,7 @@ interface Registered {
894
1026
  reference: WrapperReference;
895
1027
  form: PyWrapperForm;
896
1028
  }
1029
+ /** A wrapper form with the pattern and pack that declare it. */
897
1030
  interface FormOf {
898
1031
  pack: PythonPack;
899
1032
  pattern: PythonDiscoveryPattern;
@@ -909,7 +1042,15 @@ declare class PythonWrapperIndex {
909
1042
  private readonly ownRoutes;
910
1043
  private readonly unitsByKey;
911
1044
  private readonly unitsByFile;
1045
+ /** Told about every registration, so a cache can repeat the ones a file's discovery made. */
1046
+ onRegistered: ((target: ReachedFunction, declared: FormOf) => void) | null;
912
1047
  constructor(options: WrapperIndexOptions, filesByPath: ReadonlyMap<string, BoundPythonFile>);
1048
+ /**
1049
+ * The same lookup `wrappersFor` noted under `id`, made again over this
1050
+ * index, or null when `id` is not one of these.
1051
+ */
1052
+ lookAgain(id: string): string | null;
1053
+ private noted;
913
1054
  /** The project function a module-level name in this file refers to, or null for anything else. */
914
1055
  functionCalled(file: string, name: string): ReachedFunction | null;
915
1056
  register(covers: {
@@ -934,30 +1075,29 @@ declare class PythonWrapperIndex {
934
1075
  }
935
1076
 
936
1077
  /**
937
- * Finds decorated routes and turns each one into a `RawCodeStructure`.
938
- *
939
- * Only decorated module-level functions and classes that are declared
940
- * unconditionally get discovered, the same boundary the binder in scope.ts
941
- * draws. Nothing here reads into a unit's body, so a route's summary comes out
942
- * with no branches at all, or with exactly one branch describing what an
943
- * annotation or a decorator keyword already states, such as a FastAPI
944
- * `response_model` or `status_code`, or a return annotation.
1078
+ * Finds decorated routes and turns each one into a `RawCodeStructure`, and
1079
+ * collects the client units for outgoing requests alongside them.
945
1080
  *
946
- * That is enough to pair a route against a caller by method and path, and it
947
- * claims nothing about behavior nobody read.
1081
+ * Every decorated definition in the file is classified, wherever it is
1082
+ * written. One a pack's pattern accepts becomes a unit per HTTP method and
1083
+ * per mount. The path is the decorator's first argument with the router
1084
+ * prefixes in front of it. The status and body shape come from decorator
1085
+ * keywords, the return annotation, and each return or raise in the body.
1086
+ * A route that cannot be read keeps its name and gets no path, so it pairs
1087
+ * with nothing.
948
1088
  */
949
1089
 
950
1090
  interface DiscoveryOptions {
951
1091
  packs: PythonPack[];
952
1092
  /** Repo-relative or absolute path recorded on each summary's `location.file`. */
953
1093
  filePath: string;
954
- /** Without it, every route object looks like the app itself and paths stand as written. */
1094
+ /** Without it, every route object is treated as the app itself and paths are used as written. */
955
1095
  routerIndex?: RouterIndex;
956
1096
  /** Under "strict" a route whose unit cannot be built stops the run instead of abstaining. */
957
1097
  gapHandling?: ExtractorOptions["gapHandling"];
958
- /** What a pack needs to say a call talks to the database. Absent when no pack does. */
1098
+ /** What a pack needs to decide that a call talks to the database. Absent when no pack declares storage. */
959
1099
  storage?: StorageLookup | undefined;
960
- /** The file's absolute path, which module resolution wants; `filePath` may be shortened for display. */
1100
+ /** The file's absolute path, which module resolution needs. `filePath` may be shortened for display. */
961
1101
  absoluteFile?: string | undefined;
962
1102
  /** The project's facts, so the rules can say what an object no scope has a binding for was built by. */
963
1103
  facts?: Database | undefined;
@@ -968,27 +1108,33 @@ interface DiscoveryOptions {
968
1108
  }
969
1109
  declare function discoverUnits(root: Node, module: ModuleBinding, options: DiscoveryOptions): RawCodeStructure[];
970
1110
 
971
- /** Ask what these calls come down to, then derive. */
1111
+ /**
1112
+ * Runs the shared resolution rules over this project's facts. The rules in
1113
+ * `@suss/resolution` are the same ones the TypeScript adapter runs, so a
1114
+ * Python value is followed the same way a TypeScript one is.
1115
+ */
1116
+
1117
+ /** Asks the rules to resolve these keys, and derives what follows. */
972
1118
  declare function resolveCalls(db: Database, callKeys: readonly string[]): void;
973
1119
  /**
974
1120
  * The values an object contains under its own keys, in the order the source
975
1121
  * writes them. Empty when nothing said what the object contains.
976
1122
  */
977
1123
  declare function containedValues(db: Database, objectKey: string): string[];
978
- /** What a call comes down to, when the rules settled it on an object. */
1124
+ /** The object a call returns, when the rules settled it on one. */
979
1125
  declare function objectReturnedBy(db: Database, callKey: string): string | null;
980
1126
 
981
1127
  /**
982
- * values.ts: the facts @suss/resolution already joins, emitted for Python.
983
- * The relation names and shapes come from that package's own header, so a
984
- * Python value follows the same rules a TypeScript one does.
1128
+ * Emits the value facts that the rules in @suss/resolution join, read from
1129
+ * Python source. The relations are the ones that package defines, so a
1130
+ * Python value is followed by the same rules as a TypeScript one.
985
1131
  *
986
1132
  * A name is keyed by the scope that binds it. A function's own names take
987
- * the function's key, so two handlers that both write `query` stay apart,
988
- * and a module's names take the file's key, which is what another file
989
- * imports back out. Every write to a name in one scope is collected in
990
- * source order and `valueLeftByWrites` says what the name comes down to,
991
- * so a reassigned name states one value or none rather than two.
1133
+ * the function's key, so two handlers that both write `query` stay apart.
1134
+ * A module's names take the file's key, and another file's import matches
1135
+ * on that key. Every write to a name in one scope is collected in source
1136
+ * order, and `valueLeftByWrites` decides what the name comes down to, so a
1137
+ * reassigned name gets one value or none, never two.
992
1138
  */
993
1139
 
994
1140
  /**
@@ -1015,21 +1161,27 @@ declare function nameKeyIn(filePath: string, enclosing: Node | null, name: strin
1015
1161
  declare function emitValueFacts(db: Database, filePath: string, root: Node): void;
1016
1162
 
1017
1163
  /**
1018
- * The session behind `suss ask why` on a Python project: a tree-sitter
1019
- * parse of the asked-about source, a way to point at the expression
1020
- * somebody spelled, and the witness proof of what it resolved to,
1021
- * rendered through `@suss/resolution`'s phrases.
1164
+ * The session behind `suss ask why` on a Python project. It finds the
1165
+ * expression a question points at by file, line and text, and renders the
1166
+ * witness proof of what that expression resolved to through the phrases
1167
+ * in `@suss/resolution`.
1022
1168
  *
1023
- * It parses every file under the root and emits the same value facts
1024
- * `extractPythonProject` does, keeping a location for every fact key so
1025
- * a proof's atoms can point back at source. A handle this session hands
1026
- * back pairs a tree-sitter node with the file it came from, since a
1027
- * node alone does not say which file parsed it.
1169
+ * It parses every file under the root and records the same value facts
1170
+ * and pack words `extractPythonProject` does, along with a location for
1171
+ * every fact key, so each step of a proof can point back at the source. A
1172
+ * handle pairs a tree-sitter node with its file, because a node alone
1173
+ * does not say which file it was parsed from.
1028
1174
  */
1029
1175
 
1030
1176
  interface PythonWhySessionOptions {
1031
1177
  /** The project root, which paths in every answer come out relative to. */
1032
1178
  dir: string;
1179
+ /** Directories an absolute import is resolved against. Read from `dir` when absent, the way `extractPythonProject` reads them. */
1180
+ roots?: string[];
1181
+ /** Roots the project directory does not show, such as a checked-out submodule. Added after the others. */
1182
+ additionalRoots?: string[];
1183
+ /** The packs the extraction ran with. A hop only a pack declares, such as `session.get(User, id)` giving back a User, is explained only when that pack is here. */
1184
+ packs?: readonly PythonPack[];
1033
1185
  }
1034
1186
  /** A found node, paired with the file it was parsed from. */
1035
1187
  interface PythonValueHandle {
@@ -1054,8 +1206,8 @@ declare class PythonWhySession {
1054
1206
  findCallee(file: string, startLine: number, endLine: number, calleeText: string): PythonValueHandle | null;
1055
1207
  /**
1056
1208
  * Why `value` resolves to what it does: the witness proof, flattened
1057
- * to the chain and rendered. Null when the value does not resolve,
1058
- * which the caller says in its own words.
1209
+ * to the chain and rendered. Null when the value does not resolve, and
1210
+ * the caller then writes its own message.
1059
1211
  */
1060
1212
  explain(value: PythonValueHandle, options?: {
1061
1213
  maxDepth?: number;
@@ -1063,23 +1215,18 @@ declare class PythonWhySession {
1063
1215
  private locate;
1064
1216
  private rootOf;
1065
1217
  private pathOf;
1066
- /** A file path said relative to the root, or as given when it is outside the root. */
1218
+ /** A file path relative to the root, or unchanged when it is outside the root. */
1067
1219
  private displayPath;
1068
1220
  }
1069
1221
 
1070
1222
  /**
1071
- * The name is part of the key because the range is measured in lines, two
1072
- * units can start on the same line, and `entry` is a set, so keying on the range
1073
- * alone would drop one of them.
1223
+ * The facts the module resolver adds to the shared store.
1224
+ *
1225
+ * `importsModule` records each module a file imports, as written, and
1226
+ * `importsFile` the file an import resolved to. An import the resolver
1227
+ * abstains on gets no file rather than a guessed one.
1074
1228
  */
1075
- declare function unitKey(filePath: string, range: {
1076
- start: number;
1077
- end: number;
1078
- }, name: string): string;
1079
- declare function emitEntryFact(db: Database, filePath: string, range: {
1080
- start: number;
1081
- end: number;
1082
- }, name: string): void;
1229
+
1083
1230
  /**
1084
1231
  * Every import in the file, wherever it is written. Python code puts an import
1085
1232
  * inside a function to break a cycle between two modules, and a chain through
@@ -1087,17 +1234,74 @@ declare function emitEntryFact(db: Database, filePath: string, range: {
1087
1234
  */
1088
1235
  declare function emitModuleImportFacts(db: Database, filePath: string, module: ModuleBinding, resolverOptions: ModuleResolverOptions): void;
1089
1236
 
1237
+ /**
1238
+ * The directories a Python project's absolute imports resolve against:
1239
+ * the project directory, and the source directories it declares.
1240
+ *
1241
+ * A project that keeps its package under `src/` imports it as
1242
+ * `orders.routes`, because installing the project puts `src/` on
1243
+ * `sys.path`. suss cannot run the install, so it reads where
1244
+ * `pyproject.toml` tells the build backend to look, and uses `src/`
1245
+ * when that declares nothing and `src/` contains a package.
1246
+ *
1247
+ * An extra root cannot pick the wrong module, because the module
1248
+ * resolver abstains when two roots both have a file for one import.
1249
+ */
1250
+ interface UnreadManifest {
1251
+ /** The file, relative to the project root. */
1252
+ where: string;
1253
+ /** Why it could not be read, in one line. */
1254
+ reason: string;
1255
+ }
1256
+ interface PythonSourceRoots {
1257
+ /** Absolute: the project directory first, then each source directory declared or found under it. */
1258
+ roots: string[];
1259
+ /** A manifest that might have declared a source directory and could not be read. */
1260
+ unread: UnreadManifest[];
1261
+ }
1262
+ type TomlTable = Record<string, unknown>;
1263
+ type TomlFileRead = {
1264
+ kind: "parsed";
1265
+ value: unknown;
1266
+ } | {
1267
+ kind: "unreadable";
1268
+ reason: string;
1269
+ };
1270
+ declare function readTomlFile(file: string): TomlFileRead;
1271
+ declare function tableAt(value: unknown, ...keys: string[]): TomlTable | null;
1272
+ declare function pythonSourceRoots(projectRoot: string): PythonSourceRoots;
1273
+
1274
+ /**
1275
+ * The entry point for a Python run. It discovers units, assembles their
1276
+ * summaries in the shared IR, and emits the run's facts into one `Database`.
1277
+ *
1278
+ * Every file is parsed and bound first, and the value facts are emitted
1279
+ * across all of them, before discovery runs on any file, because a mount in
1280
+ * one file can refer to a router built in another. Each discovered unit
1281
+ * goes through `@suss/extractor`'s `assembleSummary`, the same assembly the
1282
+ * TypeScript adapter uses, so both languages share one gap detection. With
1283
+ * a cache, a run after an edit replays the files and functions whose
1284
+ * inputs did not change, through the records in `reuse.ts`.
1285
+ */
1286
+
1090
1287
  interface ExtractPythonOptions {
1091
1288
  /** Absolute paths of the files to parse and extract. */
1092
1289
  files: string[];
1093
1290
  packs: PythonPack[];
1094
- /** Directories an absolute import is resolved against. */
1095
- roots: string[];
1291
+ /**
1292
+ * Directories an absolute import is resolved against. When absent,
1293
+ * they are read from `projectRoot`: the directory itself and the
1294
+ * source directories its `pyproject.toml` declares, or `src/` when
1295
+ * it contains a package.
1296
+ */
1297
+ roots?: string[];
1298
+ /** Roots that cannot be found from the project directory, such as a checked-out submodule. Added after the others. */
1299
+ additionalRoots?: string[];
1096
1300
  /** When set, `location.file` on each summary is relativized against this. */
1097
1301
  workspaceRoot?: string;
1098
1302
  /** The directory a summary's id measures its file from, when that differs from `workspaceRoot`. */
1099
1303
  projectRoot?: string;
1100
- /** As well as deciding how much of what nobody could read reaches a summary, "strict" lets a route that cannot be built stop the run. */
1304
+ /** How much of what could not be read goes on a summary. "strict" also makes a route that cannot be built stop the run. */
1101
1305
  gapHandling?: ExtractorOptions["gapHandling"];
1102
1306
  /** Called once with the run's per-phase wall time, for `suss extract --timing`. */
1103
1307
  onTiming?: (report: TimingReport) => void;
@@ -1107,10 +1311,31 @@ interface ExtractPythonOptions {
1107
1311
  onCacheDiagnostic?: (diagnostic: CacheDiagnostic) => void;
1108
1312
  /** Absolute. `<projectRoot>/.suss/cache` by default; `null` turns it off. */
1109
1313
  cacheDir?: string | null;
1314
+ /** The modules the project lists in `suss.json`, with absolute paths. */
1315
+ modules?: readonly DeclaredModule[];
1316
+ /** Parses an earlier run in this process left, for a file whose text is unchanged. */
1317
+ keptParses?: KeptPythonParses;
1110
1318
  }
1319
+ /** A file's tree and the scopes bound over it, as a later run can reuse them. */
1320
+ type KeptPythonParses = KeptParses<{
1321
+ root: Node;
1322
+ module: ModuleBinding;
1323
+ }>;
1324
+ /** A holder for parses between runs, which frees each tree it lets go of. */
1325
+ declare function keptPythonParses(): KeptPythonParses;
1326
+ /**
1327
+ * Parses these files into `kept` before any run needs them. A run served
1328
+ * whole from the cache parses nothing, so a process that keeps parses
1329
+ * calls this while nobody is waiting.
1330
+ */
1331
+ declare function parsePythonAhead(files: readonly string[], kept: KeptPythonParses): Promise<void>;
1111
1332
  interface ExtractPythonResult {
1112
1333
  summaries: BehavioralSummary[];
1113
1334
  facts: Database;
1335
+ /** The roots absolute imports were resolved against. */
1336
+ roots: string[];
1337
+ /** A manifest that might have declared a source directory and could not be read. */
1338
+ unreadManifests: UnreadManifest[];
1114
1339
  }
1115
1340
  /** One parsed file and the packs a run over it would load. */
1116
1341
  interface FileFactsOptions {
@@ -1133,19 +1358,19 @@ declare function extractPythonProject(options: ExtractPythonOptions): Promise<Ex
1133
1358
  declare function findPythonFiles(root: string): string[];
1134
1359
 
1135
1360
  /**
1136
- * Evidence for drafting a project-wrapper dependency stub: every
1137
- * import, anywhere in the project, of the asked module or one of its
1138
- * submodules. A Python decorator pattern matches a wrapper's import
1139
- * module exactly (see discovery.ts's `importModule.includes`), so a
1140
- * stub covers one imported module at a time rather than one package,
1141
- * and this groups by the exact module text a project writes.
1361
+ * Every import in the project of a module or one of its submodules, for
1362
+ * drafting a stub of a project's wrapper package.
1363
+ *
1364
+ * A discovery pattern matches a decorator's import module exactly, so a
1365
+ * stub covers one imported module at a time. The sites are grouped by
1366
+ * the module text as the project wrote it for that reason.
1142
1367
  *
1143
- * This walks every node rather than going through the binder in
1144
- * scope.ts, so an import inside an `if` or `try` still counts as
1145
- * evidence even though the binder would not resolve a name through it.
1368
+ * The walk visits every node instead of asking the binder, so an import
1369
+ * inside an `if` or a `try` still counts here, even though the binder
1370
+ * does not resolve a name through it.
1146
1371
  */
1147
1372
  interface PythonImportSite {
1148
- /** The name imported at this site, null for a bare `import module`, which binds the whole module rather than one name from it. */
1373
+ /** The name imported at this site. Null for a bare `import module`, which binds the whole module. */
1149
1374
  name: string | null;
1150
1375
  file: string;
1151
1376
  line: number;
@@ -1167,13 +1392,13 @@ interface PythonImportEvidenceOptions {
1167
1392
  declare function pythonImportEvidence(options: PythonImportEvidenceOptions): Promise<PythonImportEvidence[]>;
1168
1393
 
1169
1394
  /**
1170
- * version.ts: this adapter's own half of the cache key.
1395
+ * The Python adapter's part of the extraction cache key.
1171
1396
  *
1172
- * `ADAPTER_VERSION` is the hand-bumped semver. Bump it on any change
1173
- * that affects extraction output: IR shape, discovery semantics,
1174
- * terminal classification, anything that would invalidate previously
1175
- * cached summaries.
1397
+ * Bump `ADAPTER_VERSION` by hand on any change to what extraction
1398
+ * produces, such as the IR it writes, what discovery finds, or how a
1399
+ * terminal is classified. Summaries cached under the old version are
1400
+ * then read again instead of reused.
1176
1401
  */
1177
1402
  declare const ADAPTER_VERSION = "0.1.0";
1178
1403
 
1179
- export { ADAPTER_VERSION, type AnnotationContext, type Binding, type BoundPythonFile, type DecoratedClassRoute, type DecoratedFunctionRoute, type DecoratorArg, type DecoratorClassification, type DiscoveryOptions, type ExtractPythonOptions, type ExtractPythonResult, type FileFactsOptions, type ModuleBinding, type ModuleResolution, type ModuleResolverOptions, type MountObjectCarrier, type MountObjectPrefix, type MountPrefixEffect, type NoValuePrefix, type PathRepeatedSlashes, type PrefixTrailingSlash, type PyModelEntryFunction, type PyModelEntryMethod, type PyModelQueries, type PyStatusCall, type PythonDiscoveryPattern, type PythonImportEvidence, type PythonImportEvidenceOptions, type PythonImportSite, type PythonPack, type PythonValueHandle, PythonWhySession, type PythonWhySessionOptions, type RawSqlPattern, type RelativeModuleSpec, type RouteConventions, type RoutePrefixResolution, type RouterComposition, type RouterIndex, type Scope, type ScopeKind, type SqlCallArgument, type SqlClientHandoff, type SqlClientPattern, type SqlStatementCall, type SqlTableCall, type StoragePattern, annotationToShape, bindModule, buildRouterIndex, classifyDecorator, containedValues, discoverUnits, emitEntryFact, emitModuleImportFacts, emitValueFacts, extractPythonProject, factsForFile, findPythonFiles, nameKeyIn, nodeId, objectReturnedBy, parsePython, parsePythonSync, preloadPythonGrammar, pythonImportEvidence, readKey, resolveAbsoluteModule, resolveCalls, resolveModule, resolveName, resolveRelativeModule, shapeFromName, unitKey };
1404
+ export { ADAPTER_VERSION, type AnnotationContext, type Binding, type BoundPythonFile, type DecoratedClassRoute, type DecoratedFunctionRoute, type DecoratorArg, type DecoratorClassification, type DiscoveryOptions, type ExtractPythonOptions, type ExtractPythonResult, type FileFactsOptions, type KeptPythonParses, type ModuleBinding, type ModuleResolution, type ModuleResolverOptions, type MountObjectCarrier, type MountObjectPrefix, type MountPrefixEffect, type NoValuePrefix, type ParameterSource, type PathRepeatedSlashes, type PrefixTrailingSlash, type PyModelEntryFunction, type PyModelEntryMethod, type PyModelQueries, type PyStatusCall, type PyTestCases, type PyTestMocks, type PyTestSetUp, type PythonDiscoveryPattern, type PythonImportEvidence, type PythonImportEvidenceOptions, type PythonImportSite, type PythonPack, type PythonSourceRoots, type PythonValueHandle, PythonWhySession, type PythonWhySessionOptions, type RawSqlPattern, type RelativeModuleSpec, type RequestObject, type RouteConventions, type RoutePrefixResolution, type RouterComposition, type RouterIndex, type Scope, type ScopeKind, type SqlCallArgument, type SqlClientHandoff, type SqlClientPattern, type SqlStatementCall, type SqlTableCall, type StoragePattern, type TomlFileRead, type TomlTable, type UnreadManifest, annotationToShape, bindModule, buildRouterIndex, classifyDecorator, containedValues, discoverUnits, emitModuleImportFacts, emitValueFacts, extractPythonProject, factsForFile, findPythonFiles, keptPythonParses, nameKeyIn, nodeId, objectReturnedBy, parsePython, parsePythonAhead, parsePythonSync, preloadPythonGrammar, pythonImportEvidence, pythonSourceRoots, readKey, readTomlFile, resolveAbsoluteModule, resolveCalls, resolveModule, resolveName, resolveRelativeModule, shapeFromName, tableAt };