Skip to content

Commit e357d73

Browse files
committed
Rework Python bridge
1 parent bbe500f commit e357d73

File tree

14 files changed

+208
-126
lines changed

14 files changed

+208
-126
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "fontbakery-bridge/fontbakery"]
2+
path = fontbakery-bridge/fontbakery
3+
url = https://github.com/googlefonts/fontbakery

.vscode/settings.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"rust-analyzer.cargo.features": "all",
3+
"rust-analyzer.cargo.targetDir": true
4+
}

Cargo.toml

+8-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ members = [
55
"fontspector-cli",
66
"fontspector-checkapi",
77
"fontspector-checkhelper",
8-
"profile-universal", "profile-testplugin", "profile-googlefonts",
8+
"profile-universal",
9+
"profile-testplugin",
10+
"profile-googlefonts",
911
"fontspector-web",
1012
"fontbakery-bridge",
1113
]
1214

13-
default-members = [ "fontspector-cli" ]
15+
default-members = ["fontspector-cli"]
1416

1517
[workspace.dependencies]
1618
pluginator = "1.0.1"
@@ -22,5 +24,8 @@ write-fonts = "0.27.0"
2224
font-types = "0.5.5"
2325

2426
# Serialization
25-
serde = {version = "1.0.130", features=["derive"] }
27+
serde = { version = "1.0.130", features = ["derive"] }
28+
serde_json = "1.0"
2629
fontspector-checkhelper = { path = "./fontspector-checkhelper" }
30+
31+
log = "0.4.14"

fontbakery-bridge/Cargo.toml

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ version = "0.1.0"
44
edition = "2021"
55

66
[lib]
7-
crate-type=["cdylib"]
7+
# When running as a plugin:
8+
# crate-type = ["cdylib"]
9+
# When running as a library:
10+
crate-type = ["lib"]
811

912
[dependencies]
1013
fontspector-checkapi = { path = "../fontspector-checkapi" }
1114
pyo3 = "0.22"
15+
serde_json = { workspace = true }
16+
log = { workspace = true }
1217

1318
[target.'cfg(not(target_family = "wasm"))'.dependencies]
1419
# Plugin architecture
15-
pluginator = {workspace = true}
20+
pluginator = { workspace = true }

fontbakery-bridge/fontbakery

Submodule fontbakery added at 99b13d1

fontbakery-bridge/src/checks.rs

-91
This file was deleted.

fontbakery-bridge/src/lib.rs

+156-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
#![allow(non_upper_case_globals)]
22
#![deny(clippy::unwrap_used, clippy::expect_used)]
3+
34
use fontspector_checkapi::{prelude::*, StatusCode};
4-
use pyo3::prelude::*;
5-
mod checks;
6-
struct FontbakeryBridge;
5+
use pyo3::{prelude::*, types::PyList};
6+
use serde_json::json;
7+
pub struct FontbakeryBridge;
78

89
// We isolate the Python part to avoid type/result madness.
9-
fn call_python(module: &str, function: &str, testable: &Testable) -> PyResult<CheckFnResult> {
10+
fn python_checkrunner_impl(
11+
module: &str,
12+
function: &str,
13+
testable: &Testable,
14+
) -> PyResult<CheckFnResult> {
1015
let filename = testable.filename.to_string_lossy();
1116
Python::with_gil(|py| {
1217
let module = PyModule::import_bound(py, module)?;
@@ -47,12 +52,22 @@ fn call_python(module: &str, function: &str, testable: &Testable) -> PyResult<Ch
4752
"Fontbakery returned unknown status code".to_string(),
4853
)
4954
})?;
50-
let code = value.get_item(1)?.getattr("code")?.extract::<String>()?;
51-
let message = value.get_item(1)?.getattr("message")?.extract::<String>()?;
55+
let code = if value.get_item(1)?.hasattr("code")? {
56+
Some(value.get_item(1)?.getattr("code")?.extract::<String>()?)
57+
} else {
58+
None
59+
};
60+
let message = if value.get_item(1)?.hasattr("message")? {
61+
value.get_item(1)?.getattr("message")?
62+
} else {
63+
value.get_item(1)?
64+
}
65+
.extract::<String>()?;
66+
5267
messages.push(Status {
5368
message: Some(message),
5469
severity: status,
55-
code: Some(code),
70+
code,
5671
});
5772
}
5873
Ok(return_result(messages))
@@ -61,7 +76,7 @@ fn call_python(module: &str, function: &str, testable: &Testable) -> PyResult<Ch
6176

6277
// This wrapper will work for any fontbakery check that takes a single
6378
// Font or ttFont object as an argument.
64-
fn run_a_python_test(c: &Testable, context: &Context) -> CheckFnResult {
79+
fn python_checkrunner(c: &Testable, context: &Context) -> CheckFnResult {
6580
let module = context
6681
.check_metadata
6782
.get("module")
@@ -74,27 +89,149 @@ fn run_a_python_test(c: &Testable, context: &Context) -> CheckFnResult {
7489
.ok_or_else(|| CheckError::Error("No function specified".to_string()))?
7590
.as_str()
7691
.ok_or_else(|| CheckError::Error("function in metadata was not a string!".to_string()))?;
77-
call_python(module, function, c)
92+
python_checkrunner_impl(module, function, c)
7893
.unwrap_or_else(|e| Err(CheckError::Error(format!("Python error: {}", e))))
7994
}
8095

96+
fn register_python_checks(modulename: &str, source: &str, cr: &mut Registry) -> Result<(), String> {
97+
Python::with_gil(|py| {
98+
// Assert that we have loaded the FB prelude
99+
let _prelude = PyModule::import_bound(py, "fontbakery.prelude")?;
100+
let callable = PyModule::import_bound(py, "fontbakery.callable")?;
101+
let full_source = "from fontbakery.prelude import *\n\n".to_string() + source;
102+
let module =
103+
PyModule::from_code_bound(py, &full_source, &format!("{}.py", modulename), modulename)?;
104+
// let check = module.getattr("check_hinting_impact")?;
105+
// Find all functions in the module which are checks
106+
let checktype = callable.getattr("FontBakeryCheck")?;
107+
for name in module.dir()?.iter() {
108+
let name_str: String = name.extract()?;
109+
let obj = module.getattr(name.downcast()?)?;
110+
if let Ok(true) = obj.is_instance(&checktype) {
111+
let id: String = obj.getattr("id")?.extract()?;
112+
// Check the mandatory arguments
113+
let args = obj.getattr("mandatoryArgs")?.extract::<Vec<String>>()?;
114+
if args.len() != 1 || !(args[0] == "font" || args[0] == "ttFont") {
115+
log::warn!(
116+
"Can't load check {}; unable to support arguments: {}",
117+
id,
118+
args.join(", ")
119+
);
120+
continue;
121+
}
122+
let title: String = obj.getattr("__doc__")?.extract()?;
123+
let py_rationale = obj.getattr("rationale")?;
124+
let rationale: String = if py_rationale.is_instance_of::<PyList>() {
125+
let r: Vec<String> = py_rationale.extract()?;
126+
r.join(", ")
127+
} else {
128+
py_rationale.extract()?
129+
};
130+
let py_proposal = obj.getattr("proposal")?;
131+
let proposal: String = if py_proposal.is_instance_of::<PyList>() {
132+
let r: Vec<String> = py_proposal.extract()?;
133+
r.join(", ")
134+
} else {
135+
py_proposal.extract()?
136+
};
137+
log::info!("Registered check: {}", id);
138+
let metadata = json!({
139+
"module": modulename,
140+
"function": name_str,
141+
});
142+
cr.register_check(Check {
143+
id: id.leak(),
144+
title: title.leak(),
145+
rationale: rationale.leak(),
146+
proposal: proposal.leak(),
147+
hotfix: None,
148+
fix_source: None,
149+
applies_to: "TTF",
150+
flags: CheckFlags::default(),
151+
implementation: CheckImplementation::CheckOne(&python_checkrunner),
152+
_metadata: Some(metadata.to_string().leak()),
153+
})
154+
}
155+
}
156+
Ok(())
157+
})
158+
.map_err(|e: PyErr| format!("Error loading checks: {}", e))
159+
}
160+
81161
impl fontspector_checkapi::Plugin for FontbakeryBridge {
82162
fn register(&self, cr: &mut Registry) -> Result<(), String> {
83-
cr.register_check(checks::hinting_impact);
84-
cr.register_check(checks::opentype_name_empty_records);
85-
cr.register_check(checks::monospace);
86163
pyo3::prepare_freethreaded_python();
164+
// Load needed FB modules
165+
let ok: PyResult<()> = Python::with_gil(|py| {
166+
PyModule::from_code_bound(
167+
py,
168+
include_str!("../fontbakery/Lib/fontbakery/callable.py"),
169+
"callable.py",
170+
"fontbakery.callable",
171+
)?;
172+
PyModule::from_code_bound(
173+
py,
174+
include_str!("../fontbakery/Lib/fontbakery/status.py"),
175+
"status.py",
176+
"fontbakery.status",
177+
)?;
178+
PyModule::from_code_bound(
179+
py,
180+
include_str!("../fontbakery/Lib/fontbakery/message.py"),
181+
"message.py",
182+
"fontbakery.message",
183+
)?;
184+
Ok(())
185+
});
186+
ok.map_err(|e| format!("Error loading FB modules: {}", e))?;
187+
188+
register_python_checks(
189+
"fontbakery.checks.opentype.kern",
190+
include_str!("../fontbakery/Lib/fontbakery/checks/opentype/kern.py"),
191+
cr,
192+
)?;
193+
register_python_checks(
194+
"fontbakery.checks.opentype.cff",
195+
include_str!("../fontbakery/Lib/fontbakery/checks/opentype/cff.py"),
196+
cr,
197+
)?;
198+
register_python_checks(
199+
"fontbakery.checks.opentype.fvar",
200+
include_str!("../fontbakery/Lib/fontbakery/checks/opentype/fvar.py"),
201+
cr,
202+
)?;
203+
register_python_checks(
204+
"fontbakery.checks.opentype.gdef",
205+
include_str!("../fontbakery/Lib/fontbakery/checks/opentype/gdef.py"),
206+
cr,
207+
)?;
208+
register_python_checks(
209+
"fontbakery.checks.opentype.gpos",
210+
include_str!("../fontbakery/Lib/fontbakery/checks/opentype/gpos.py"),
211+
cr,
212+
)?;
213+
register_python_checks(
214+
"fontbakery.checks.opentype.head",
215+
include_str!("../fontbakery/Lib/fontbakery/checks/opentype/head.py"),
216+
cr,
217+
)?;
218+
register_python_checks(
219+
"fontbakery.checks.opentype.hhea",
220+
include_str!("../fontbakery/Lib/fontbakery/checks/opentype/hhea.py"),
221+
cr,
222+
)?;
87223
cr.register_profile(
88224
"fontbakery",
89225
Profile::from_toml(
90226
r#"
91-
[sections]
92-
"Test profile" = [
93-
"hinting_impact",
94-
"opentype/name/empty_records",
95-
"opentype/monospace",
96-
]
97-
"#,
227+
[sections]
228+
"Test profile" = [
229+
"hinting_impact",
230+
"opentype/name/empty_records",
231+
"opentype/monospace",
232+
"opentype/cff_call_depth",
233+
]
234+
"#,
98235
)
99236
.map_err(|_| "Couldn't parse profile")?,
100237
)

fontspector-checkapi/Cargo.toml

+7-6
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,22 @@ crate-type = ["cdylib", "rlib"]
88

99
[target.'cfg(not(target_family = "wasm"))'.dependencies]
1010
# Plugin architecture
11-
pluginator = {workspace = true}
11+
pluginator = { workspace = true }
1212

1313
[dependencies]
1414
font-types = { workspace = true }
15-
read-fonts = {workspace = true}
15+
read-fonts = { workspace = true }
1616
write-fonts = { workspace = true }
17-
skrifa = {workspace = true}
18-
fontspector-checkhelper = {workspace = true}
17+
skrifa = { workspace = true }
18+
fontspector-checkhelper = { workspace = true }
19+
log = { workspace = true }
1920

2021
# Filetype
2122
glob-match = "0.2.1"
22-
glob="0.3.1"
23+
glob = "0.3.1"
2324

2425
# Needed so that we can refer to status codes on the command line
25-
clap = {version = "3.2.5", features = ["derive"]}
26+
clap = { version = "3.2.5", features = ["derive"] }
2627

2728
# Serializing and deserializing profiles
2829
toml = "0.8.14"

0 commit comments

Comments
 (0)