@descryy/adapter-python 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "name": "@descryy/adapter-python",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Python adapter. Parses with Python's own ast in one subprocess (DEC-062) \u2014 no grammar to keep in sync, and annotations arrive as source text.",
6
6
  "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/descryhq-wq/descry-adapters.git",
10
+ "directory": "packages/adapter-python"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/descryhq-wq/descry-adapters/issues"
14
+ },
15
+ "homepage": "https://github.com/descryhq-wq/descry-adapters#readme",
7
16
  "engines": {
8
17
  "node": ">=22.5"
9
18
  },
@@ -26,9 +35,9 @@
26
35
  "build:assets": "node -e \"require('fs').copyFileSync('src/extract.py','dist/extract.py')\""
27
36
  },
28
37
  "dependencies": {
29
- "@descryy/adapter-common": "0.1.0",
30
- "@descryy/ir": "0.4.0",
31
- "@descryy/adapter-openapi": "0.2.0",
32
- "@descryy/adapter-sql": "0.2.0"
38
+ "@descryy/adapter-common": "0.2.0",
39
+ "@descryy/ir": "0.5.0",
40
+ "@descryy/adapter-openapi": "0.3.0",
41
+ "@descryy/adapter-sql": "0.3.0"
33
42
  }
34
43
  }
package/src/extract.py CHANGED
@@ -290,6 +290,17 @@ class Collector(ast.NodeVisitor):
290
290
  # Module-level `name = ClassName()`, which is the only inference this
291
291
  # file performs and the only one it can perform without a checker.
292
292
  self.instantiations: list[dict] = []
293
+ # A local variable's *own* type annotation — `org: Organization | None
294
+ # = await get_org_for_user(...)` — as opposed to `instantiations`
295
+ # above, which only sees a bare constructor call on the right-hand
296
+ # side. The ground truth this exists for is exactly this shape: a
297
+ # local carrying its own annotation, assigned from a call whose
298
+ # return type nothing here resolves. Kept as the raw annotation
299
+ # string (not a single type name) so the Node side can run its own
300
+ # `typeNamesIn` + "exactly one candidate" discipline against
301
+ # `Optional[X]` / `X | None` / `Union[X, Y]`, the same as it already
302
+ # does for a parameter's own annotation.
303
+ self.local_annotations: list[dict] = []
293
304
  # Module-level assignments that *might* be type aliases. Classified in
294
305
  # `collect()` rather than here, because rule 4 asks where a constructor
295
306
  # was imported from and the import table is only complete once the whole
@@ -682,6 +693,20 @@ class Collector(ast.NodeVisitor):
682
693
  "line": node.lineno,
683
694
  }
684
695
  )
696
+ # A local variable's own annotation, inside a function (or
697
+ # nested closure) body — `org: Organization | None = await
698
+ # get_org_for_user(...)`. Module-level annotated assignments are
699
+ # already covered by `alias_candidates`/`_alembic_revision`; this
700
+ # is specifically the local-scope case those do not read.
701
+ if self._scope:
702
+ self.local_annotations.append(
703
+ {
704
+ "local": node.target.id,
705
+ "annotation": _annotation(node.annotation),
706
+ "scope": list(self._scope),
707
+ "line": node.lineno,
708
+ }
709
+ )
685
710
  if not self._scope and node.value is not None:
686
711
  self.alias_candidates.append(
687
712
  {
@@ -1210,19 +1235,34 @@ def _alembic_revision(tree: ast.Module) -> dict | None:
1210
1235
  down_revision: str | list[str] | None = None
1211
1236
 
1212
1237
  for node in tree.body:
1213
- if not (isinstance(node, ast.Assign) and len(node.targets) == 1):
1238
+ target: ast.AST | None
1239
+ value: ast.AST | None
1240
+ if isinstance(node, ast.Assign) and len(node.targets) == 1:
1241
+ target = node.targets[0]
1242
+ value = node.value
1243
+ elif isinstance(node, ast.AnnAssign):
1244
+ # `revision: str = "..."` — a type-annotated assignment. Alembic's
1245
+ # newer templates and mypy-strict repos write the pair this way;
1246
+ # it is semantically the same assignment, just with an annotation
1247
+ # attached. `node.value` is `None` for a bare annotation with no
1248
+ # value (`down_revision: str` alone) — that assigns nothing, so it
1249
+ # must not be treated as `down_revision = None` (a root migration
1250
+ # is a real, distinct fact). Skip the node entirely in that case.
1251
+ if node.value is None:
1252
+ continue
1253
+ target = node.target
1254
+ value = node.value
1255
+ else:
1214
1256
  continue
1215
- target = node.targets[0]
1216
1257
  if not isinstance(target, ast.Name):
1217
1258
  continue
1218
1259
 
1219
1260
  if target.id == "revision":
1220
- revision = string_of(node.value)
1261
+ revision = string_of(value)
1221
1262
  continue
1222
1263
 
1223
1264
  if target.id == "down_revision":
1224
1265
  has_down_revision = True
1225
- value = node.value
1226
1266
  if isinstance(value, ast.Constant) and value.value is None:
1227
1267
  down_revision = None
1228
1268
  else:
@@ -1301,6 +1341,7 @@ def collect(path: str, source: str, deep_call_modules: frozenset[str] = frozense
1301
1341
  "imports": collector.imports,
1302
1342
  "references": collector.references,
1303
1343
  "instantiations": collector.instantiations,
1344
+ "localAnnotations": collector.local_annotations,
1304
1345
  "valueRefs": collector.value_refs,
1305
1346
  "aliases": aliases,
1306
1347
  "calls": collector.calls,