Skip to content

Commit 4734fef

Browse files
authored
Merge pull request #36 from multimeric/id-mapping-typing
ID mapping typing
2 parents 2ba4238 + cc9e523 commit 4734fef

9 files changed

Lines changed: 1261 additions & 799 deletions

File tree

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
poetry-version: '1.5.1'
2323

2424
- name: Install dependencies
25-
run: poetry install --with lint --with tests
25+
run: poetry install --all-extras
2626

2727
- uses: pre-commit/action@v3.0.0
2828

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
### 1.4.0
55

6+
#### Added
7+
8+
* The `taxon_id` argument to `IdMapper.submit` [[#36]](https://github.com/multimeric/Unipressed/pull/36)
9+
* Detailed type annotations for `IdMapper.submit`, that enforce only certain pairs of `source`/`dest` databases
10+
611
#### Changed
712

813
* Auto-generated type definitions for the datasets have been regenerated [[#37](https://github.com/multimeric/Unipressed/pull/37)]. This pulls upstream changes from Uniprot. For a full list of changes [view this commit diff](https://github.com/multimeric/Unipressed/pull/31/commits/7e620c46175b6ec03e073fc78444a43e96821c31).

codegen/dataset/generate_fields.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
UniprotSearchField,
2323
)
2424

25+
2526
# If the functions return anything, print it
2627
app = typer.Typer(result_callback=lambda x: print(x))
2728

codegen/id_mapping/generate.py

Lines changed: 143 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,173 @@
11
import ast
2+
import sys
3+
from collections import defaultdict
4+
from dataclasses import dataclass, field
5+
from typing import List, Optional
26

37
import black
48
import requests
59
import typer
610

7-
from codegen.util import make_literal
8-
911
app = typer.Typer(result_callback=lambda x: print(x))
1012

1113

14+
def make_function(
15+
source_type: ast.expr, dest_type: ast.expr, taxon_id: bool, overload: bool = True
16+
) -> ast.FunctionDef:
17+
"""
18+
Makes a `submit()` function definition, as used by the ID mapper
19+
20+
Params:
21+
source_type: Type of the `source` argument
22+
dest_type: Type of the `dest` argument
23+
taxon_id: If true, include the `taxon_id` parameter
24+
overload: If true, this is a function overload
25+
"""
26+
args: List[ast.arg] = [
27+
ast.arg(
28+
arg="cls",
29+
),
30+
# source: Literal[...]
31+
ast.arg(
32+
arg="source",
33+
annotation=source_type,
34+
),
35+
# source: dest[...]
36+
ast.arg(
37+
arg="dest",
38+
annotation=dest_type,
39+
),
40+
# ids: Iterable[str]
41+
ast.arg(
42+
"ids",
43+
ast.Subscript(ast.Name("Iterable"), ast.Name("str")),
44+
),
45+
]
46+
defaults: list[Optional[ast.expr]] = [None, None, None]
47+
48+
if taxon_id:
49+
# taxon_id: Optional[str] = None
50+
args.append(
51+
# taxon_id: bool
52+
ast.arg(
53+
"taxon_id",
54+
annotation=ast.Subscript(ast.Name("Optional"), ast.Name("int")),
55+
)
56+
)
57+
defaults.append(ast.Constant(None))
58+
59+
decorator_list: list[ast.expr] = [
60+
ast.Name("classmethod"),
61+
]
62+
if overload:
63+
decorator_list.append(
64+
ast.Name("overload"),
65+
)
66+
67+
return ast.FunctionDef(
68+
name=f"submit",
69+
args=ast.arguments(
70+
posonlyargs=[], args=args, kwonlyargs=[], kw_defaults=[], defaults=defaults # type: ignore
71+
),
72+
body=[ast.Expr(ast.Constant(value=...))],
73+
decorator_list=decorator_list,
74+
)
75+
76+
77+
@dataclass
78+
class Rule:
79+
"""
80+
Represents a "rule" in the Uniprot API terminology, which is a method overload
81+
in the Unipressed world. A rule is a set of allowed conversions from one database
82+
to another.
83+
"""
84+
85+
#: Rule ID
86+
id: int = 0
87+
#: List of databases that can be converted to, in this rule
88+
tos: list[ast.Constant] = field(default_factory=list)
89+
#: List of databases that can be converted from, in this rule
90+
froms: list[ast.Constant] = field(default_factory=list)
91+
#: Whether this rule supports specifying the taxon ID
92+
taxon_id: bool = False
93+
94+
def to_function(self) -> ast.FunctionDef:
95+
return make_function(
96+
source_type=ast.Subscript(
97+
value=ast.Name("Literal"),
98+
slice=ast.Tuple(elts=self.froms),
99+
),
100+
dest_type=ast.Subscript(
101+
value=ast.Name("Literal"),
102+
slice=ast.Tuple(elts=self.tos), # type: ignore
103+
),
104+
taxon_id=self.taxon_id,
105+
overload=True,
106+
)
107+
108+
109+
# ast.unparse uses Python 3.9
110+
assert sys.version_info >= (3, 9)
111+
112+
12113
@app.command()
13-
def main():
14-
froms: list[ast.Constant] = []
15-
tos: list[ast.Constant] = []
114+
def main() -> None:
115+
rules: defaultdict[int, Rule] = defaultdict(Rule)
16116

17-
for group in requests.get(
117+
# Build up a list of rules
118+
type_info = requests.get(
18119
"https://rest.uniprot.org/configure/idmapping/fields"
19-
).json()["groups"]:
20-
for item in group["items"]:
120+
).json()
121+
for group_info in type_info["groups"]:
122+
for item in group_info["items"]:
21123
if item["from"]:
22-
froms.append(ast.Constant(item["name"]))
23-
if item["to"]:
24-
tos.append(ast.Constant(item["name"]))
124+
rules[item["ruleId"]].froms.append(ast.Constant(item["name"]))
125+
for rule_info in type_info["rules"]:
126+
rule = rules[rule_info["ruleId"]]
127+
for to in rule_info["tos"]:
128+
rule.tos.append(ast.Constant(to))
129+
rule.taxon_id = rule_info["taxonId"]
130+
rule.id = rule_info["ruleId"]
25131

132+
# Create a class that has one method overload per rule
26133
module = ast.Module(
27134
body=[
28135
ast.ImportFrom(
29136
module="typing_extensions",
30137
names=[
31138
ast.alias("Literal"),
32-
ast.alias("TypeAlias"),
139+
ast.alias("overload"),
33140
],
34141
level=0,
35142
),
36-
make_literal(ast.Name("From"), froms),
37-
make_literal(ast.Name("To"), tos),
143+
ast.ImportFrom(
144+
module="typing",
145+
names=[
146+
ast.alias("Iterable"),
147+
ast.alias("Optional"),
148+
],
149+
level=0,
150+
),
151+
ast.ClassDef(
152+
name="SubmitDummyClass",
153+
body=[
154+
*[rule.to_function() for rule in rules.values()],
155+
make_function(
156+
source_type=ast.Name("str"),
157+
dest_type=ast.Name("str"),
158+
taxon_id=True,
159+
overload=False,
160+
),
161+
],
162+
decorator_list=[],
163+
bases=[],
164+
keywords=[],
165+
),
38166
],
39167
type_ignores=[],
40168
)
41169

170+
# Produce the formatted output
42171
print(
43172
black.format_file_contents(
44173
ast.unparse(ast.fix_missing_locations(module)),

0 commit comments

Comments
 (0)