Skip to content

Commit

Permalink
Add Python API
Browse files Browse the repository at this point in the history
  • Loading branch information
simoncozens committed Dec 9, 2024
1 parent d111277 commit 057de1e
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 44 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]
resolver = "2"

members = ["shaperglot-lib", "shaperglot-cli", "shaperglot-web" ]
members = ["shaperglot-lib", "shaperglot-cli", "shaperglot-web", "shaperglot-py" ]

default-members = ["shaperglot-lib", "shaperglot-cli"]

Expand All @@ -11,4 +11,4 @@ skrifa = "0.26.1"
itertools = "0.13.0"
google-fonts-languages = "*"
toml = "0.8.19"
serde_json = { version = "1.0.133", features = ["preserve_order"] }
serde_json = { version = "1.0.133", features = ["preserve_order"] }
14 changes: 14 additions & 0 deletions shaperglot-py/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "shaperglot-py"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "shaperglot"
crate-type = ["cdylib"]

[dependencies]
shaperglot = { path = "../shaperglot-lib" }
pyo3 = "0.22"
pythonize = "0.22.0"
17 changes: 17 additions & 0 deletions shaperglot-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[build-system]
requires = ["maturin>=1.3,<2.0"]
build-backend = "maturin"

[project]
name = "shaperglot"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[tool.maturin]
features = ["pyo3/extension-module"]
module-name = "shaperglot._shaperglot"
python-source = "python"
1 change: 1 addition & 0 deletions shaperglot-py/python/shaperglot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pkg_resources import DistributionNotFound, get_distribution
from shaperglot._shaperglot import Checker, Languages, Reporter # , Result

try:
__version__ = get_distribution('shaperglot').version
Expand Down
1 change: 1 addition & 0 deletions shaperglot-py/python/shaperglot/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def main(args=None) -> None:
subparsers = parser.add_subparsers(help='sub-commands')

parser_describe = subparsers.add_parser('describe', help=describe.__doc__)
parser_describe.add_argument('--verbose', '-v', action='count')
parser_describe.add_argument(
'lang', metavar='LANG', help='an ISO639-3 language code'
)
Expand Down
31 changes: 16 additions & 15 deletions shaperglot-py/python/shaperglot/cli/check.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from collections import defaultdict
from typing import Optional

from shaperglot.checker import Checker
from shaperglot.languages import Languages
from shaperglot.reporter import Reporter
from shaperglot import Checker, Languages, Reporter

try:
import glyphsets
Expand Down Expand Up @@ -45,25 +43,28 @@ def check(options) -> None:
print(f"Language '{orig_lang}' not known")
continue

results = checker.check(langs[lang])
reporter = checker.check(langs[lang])

if results.is_unknown:
if reporter.is_unknown:
print(f"Cannot determine whether font supports language '{lang}'")
elif results.is_nearly_success(options.nearly):
print(f"Font nearly supports language '{lang}'")
for fixtype, things in results.unique_fixes().items():
elif reporter.is_nearly_success(options.nearly):
print(f"Font nearly supports language '{lang}' {reporter.score:.1f}%")
for fixtype, things in reporter.unique_fixes().items():
fixes_needed[fixtype].update(things)
elif results.is_success:
elif reporter.is_success:
print(f"Font supports language '{lang}'")
else:
print(f"Font does not fully support language '{lang}'")
print(
f"Font does not fully support language '{lang}' {reporter.score:.1f}%"
)

if options.verbose and options.verbose > 1:
for message in results:
print(f" * {message}")
elif options.verbose or not results.is_success:
for message in results.fails:
print(f" * {message}")
for result in reporter:
print(f" * {result.message} {result.status_code}")
elif options.verbose or not reporter.is_success:
for result in reporter:
if not result.is_success:
print(f" * {result}")

if fixes_needed:
show_how_to_fix(fixes_needed)
Expand Down
18 changes: 14 additions & 4 deletions shaperglot-py/python/shaperglot/cli/describe.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from textwrap import fill

from shaperglot.languages import Languages
from shaperglot import Languages


def describe(options) -> None:
Expand All @@ -22,17 +22,27 @@ def describe(options) -> None:
return
else:
lang = langs[options.lang]
print(f"To test for {lang['name']} support, shaperglot will:")
print(f"To test for {lang['name']} support:")
try:
width = os.get_terminal_size()[0]
except OSError:
width = 80
for shaperglot_check in lang.get("shaperglot_checks", []):
for shaperglot_check in lang.checks:
print(
fill(
"ensure " + shaperglot_check.describe(),
shaperglot_check.description,
initial_indent=" * ",
subsequent_indent=" ",
width=width - 2,
)
)
if options.verbose:
for implementation in shaperglot_check.implementations:
print(
fill(
"check " + implementation,
initial_indent=" - ",
subsequent_indent=" ",
width=width - 4,
)
)
40 changes: 23 additions & 17 deletions shaperglot-py/python/shaperglot/cli/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from textwrap import fill
from typing import Iterable

from shaperglot.checker import Checker
from shaperglot.languages import Languages
from shaperglot.reporter import Result
from shaperglot import Checker
from shaperglot import Languages

# from shaperglot import Result


try:
Expand Down Expand Up @@ -61,11 +62,13 @@ def report(options) -> None:
fixes_needed[fixtype].update(things)
if options.group:
continue
print(f"Font {msg} language '{lang}' ({langs[lang]['name']})")
print(
f"Font {msg} language '{lang}' ({langs[lang]['name']}) ({results.score:.1f}%)"
)

if options.verbose and options.verbose > 1:
for status, message in results:
print(f" * {status.value}: {message}")
for subresult in results:
print(f" * {subresult.status_code}: {subresult.message}")

if options.csv:
return
Expand Down Expand Up @@ -104,7 +107,7 @@ def short_summary(supported, nearly, unsupported) -> None:
if supported:
print(f"* {len(supported)} languages supported")
if nearly:
print(f"* {len(supported)} languages nearly supported")
print(f"* {len(nearly)} languages nearly supported")


def long_summary(fixes_needed, unsupported) -> None:
Expand All @@ -124,21 +127,24 @@ def long_summary(fixes_needed, unsupported) -> None:
print(" - " + fix)


def report_csv(langcode, lang, results: Iterable[Result]) -> None:
def report_csv(langcode, lang, results) -> None:
print(f"{langcode},\"{lang['name']}\",{results.is_success},", end="")
missing_bases = set()
missing_marks = set()
missing_anchors = set()
other_errors = set()
for msg in results:
if msg.result_code == "bases-missing":
missing_bases |= set(msg.context["glyphs"])
elif msg.result_code == "marks-missing":
missing_marks |= set(msg.context["glyphs"])
elif msg.result_code == "orphaned-mark":
missing_anchors.add(msg.context["base"] + "/" + msg.context["mark"])
else:
other_errors.add(msg.result_code)
for result in results:
for problem in result.problems:
if problem.code == "bases-missing":
missing_bases |= set(problem.context["glyphs"])
elif problem.code == "marks-missing":
missing_marks |= set(problem.context["glyphs"])
elif problem.code == "orphaned-mark":
missing_anchors.add(
problem.context["base"] + "/" + problem.context["mark"]
)
else:
other_errors.add(problem.code)
print(" ".join(sorted(missing_bases)), end=",")
print(" ".join(sorted(missing_marks)), end=",")
print(" ".join(sorted(missing_anchors)), end=",")
Expand Down
10 changes: 4 additions & 6 deletions shaperglot-py/python/shaperglot/cli/whatuses.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import sys
from textwrap import wrap

from shaperglot.languages import Languages
from shaperglot.checks.orthographies import parse_bases
from shaperglot import Languages


def whatuses(options) -> None:
Expand All @@ -28,10 +27,9 @@ def whatuses(options) -> None:
mark_langs = []
aux_langs = []
for lang in langs.values():
exemplar_chars = lang.get("exemplarChars", {})
marks = exemplar_chars.get("marks", "").replace("◌", "").split() or []
bases = parse_bases(exemplar_chars.get("base", ""))
aux = parse_bases(exemplar_chars.get("auxiliary", ""))
bases = lang.bases
marks = lang.marks
aux = lang.auxiliaries
lang_key = f"{lang['name']} [{lang['id']}]".replace(" ", "\u00A0")
if char in bases:
base_langs.append(lang_key)
Expand Down
23 changes: 23 additions & 0 deletions shaperglot-py/src/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use ::shaperglot::Check as RustCheck;
use pyo3::prelude::*;
use shaperglot::checks::CheckImplementation;

#[pyclass(module = "shaperglot")]
pub(crate) struct Check(pub(crate) RustCheck);

#[pymethods]
impl Check {
#[getter]
fn description(&self) -> String {
self.0.description.to_string()
}

#[getter]
fn implementations(&self) -> Vec<String> {
self.0
.implementations
.iter()
.map(|s| s.describe())
.collect()
}
}
29 changes: 29 additions & 0 deletions shaperglot-py/src/checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::{language::Language, reporter::Reporter};
use ::shaperglot::Checker as RustChecker;
use pyo3::{exceptions::PyValueError, prelude::*};

use std::sync::Arc;

#[pyclass(module = "shaperglot")]
pub(crate) struct Checker(Vec<u8>);

impl Checker {
pub(crate) fn _checker(&self) -> Result<Arc<RustChecker>, PyErr> {
Ok(Arc::new(RustChecker::new(&self.0).map_err(|e| {
PyErr::new::<PyValueError, _>(e.to_string())
})?))
}
}

#[pymethods]
impl Checker {
#[new]
pub(crate) fn new(filename: &str) -> Result<Self, PyErr> {
let font_binary = std::fs::read(filename)?;
Ok(Self(font_binary))
}

pub(crate) fn check(&self, lang: &Language) -> PyResult<Reporter> {
Ok(self._checker()?.check(&lang.0).into())
}
}
64 changes: 64 additions & 0 deletions shaperglot-py/src/checkresult.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use ::shaperglot::{CheckResult as RustCheckResult, Problem as RustProblem, ResultCode};
use pyo3::prelude::*;
use pythonize::pythonize;

#[pyclass(module = "shaperglot")]
pub(crate) struct CheckResult(pub(crate) RustCheckResult);

#[pymethods]
impl CheckResult {
#[getter]
pub(crate) fn message(&self) -> String {
self.0.to_string()
}

pub(crate) fn __str__(&self) -> String {
self.0.to_string()
}

#[getter]
pub(crate) fn is_success(&self) -> bool {
self.0.status == ResultCode::Pass
}

#[getter]
pub(crate) fn status_code(&self) -> String {
self.0.status.to_string()
}

#[getter]
pub(crate) fn problems(&self) -> Vec<Problem> {
self.0.problems.iter().map(|p| Problem(p.clone())).collect()
}
}

#[pyclass(module = "shaperglot")]
pub(crate) struct Problem(pub(crate) RustProblem);
#[pymethods]
impl Problem {
#[getter]
fn check_name(&self) -> String {
self.0.check_name.to_string()
}

#[getter]
fn message(&self) -> String {
self.0.message.to_string()
}

#[getter]
fn code(&self) -> String {
self.0.code.to_string()
}

#[getter]
fn terminal(&self) -> bool {
self.0.terminal
}

#[getter]
fn context<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>, PyErr> {
pythonize(py, &self.0.context)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyTypeError, _>(e.to_string()))
}
}
Loading

0 comments on commit 057de1e

Please sign in to comment.