Add Python wrapper built using PyO3

It's missing logging integration, specific error types, and the ability to retrieve loaded plugin data.
This commit is contained in:
Oliver Hamlet
2025-03-25 22:02:33 +00:00
parent 4f53341950
commit d71a207c57
12 changed files with 2076 additions and 1 deletions
Generated
+110
View File
@@ -517,6 +517,12 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.7.1"
@@ -527,6 +533,12 @@ dependencies = [
"hashbrown 0.15.2",
]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "keyvalues-parser"
version = "0.2.0"
@@ -595,6 +607,14 @@ dependencies = [
"unicase",
]
[[package]]
name = "libloot_pyo3"
version = "0.25.5"
dependencies = [
"libloot",
"pyo3",
]
[[package]]
name = "libredox"
version = "0.1.3"
@@ -645,6 +665,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -784,6 +813,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@@ -811,6 +846,69 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f1c6c3591120564d64db2261bec5f910ae454f01def849b9c22835a84695e86"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9b6c2b34cf71427ea37c7001aefbaeb85886a074795e35f161f5aecc7620a7a"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5507651906a46432cdda02cd02dd0319f6064f1374c9147c45b978621d2c3a9c"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d394b5b4fd8d97d48336bb0dd2aebabad39f1d294edd6bcd2cccf2eefe6f42"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd72da09cfa943b1080f621f024d2ef7e2773df7badd51aa30a2be1f8caa7c8e"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.38"
@@ -1074,6 +1172,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "tempfile"
version = "3.17.1"
@@ -1199,6 +1303,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
name = "version_check"
version = "0.9.5"
+1 -1
View File
@@ -26,4 +26,4 @@ rstest_reuse = "0.7.0"
tempfile = "3.17.1"
[workspace]
members = ["cxx"]
members = ["cxx", "pyo3"]
+181
View File
@@ -0,0 +1,181 @@
# This file is autogenerated by maturin v1.8.3
# To update, run
#
# maturin generate-ci github
#
name: CI
on:
push:
branches:
- main
- master
tags:
- '*'
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-22.04
target: x86_64
- runner: ubuntu-22.04
target: x86
- runner: ubuntu-22.04
target: aarch64
- runner: ubuntu-22.04
target: armv7
- runner: ubuntu-22.04
target: s390x
- runner: ubuntu-22.04
target: ppc64le
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-linux-${{ matrix.platform.target }}
path: dist
musllinux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-22.04
target: x86_64
- runner: ubuntu-22.04
target: x86
- runner: ubuntu-22.04
target: aarch64
- runner: ubuntu-22.04
target: armv7
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: musllinux_1_2
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-musllinux-${{ matrix.platform.target }}
path: dist
windows:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: windows-latest
target: x64
- runner: windows-latest
target: x86
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
architecture: ${{ matrix.platform.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-windows-${{ matrix.platform.target }}
path: dist
macos:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: macos-13
target: x86_64
- runner: macos-14
target: aarch64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.platform.target }}
path: dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist
release:
name: Release
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
needs: [linux, musllinux, windows, macos, sdist]
permissions:
# Use to sign the release artifacts
id-token: write
# Used to upload release artifacts
contents: write
# Used to generate artifact attestation
attestations: write
steps:
- uses: actions/download-artifact@v4
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-path: 'wheels-*/*'
- name: Publish to PyPI
if: ${{ startsWith(github.ref, 'refs/tags/') }}
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing wheels-*/*
+72
View File
@@ -0,0 +1,72 @@
/target
# Byte-compiled / optimized / DLL files
__pycache__/
.pytest_cache/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
.venv/
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
include/
man/
venv/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-selfcheck.json
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
.DS_Store
# Sphinx documentation
docs/_build/
# PyCharm
.idea/
# VSCode
.vscode/
# Pyenv
.python-version
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "libloot_pyo3"
version = "0.25.5"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "loot"
crate-type = ["cdylib"]
[dependencies]
libloot = { path = ".." }
pyo3 = "0.24.0"
+60
View File
@@ -0,0 +1,60 @@
# libloot-pyo3
An **incomplete** and **experimental** Python wrapper around the libloot Rust implementation, built using [PyO3](https://pyo3.rs).
## Current coverage
- [x] `LIBLOOT_VERSION_MAJOR`
- [x] `LIBLOOT_VERSION_MINOR`
- [x] `LIBLOOT_VERSION_PATCH`
- [x] `is_compatible()`
- [x] `libloot_revision()`
- [x] `libloot_version()`
- [ ] `set_logging_callback()`
- [ ] `set_log_level()`
- [x] `EdgeType`
- [x] `GameType`
- [ ] `LogLevel`
- [x] `Database`
- [ ] `Game`
- [x] `Plugin`
- [x] `Vertex`
- [x] `File`
- [x] `Filename`
- [x] `Group`
- [x] `Location`
- [x] `Message`
- [x] `MessageContent`
- [x] `PluginCleaningData`
- [x] `PluginMetadata`
- [x] `Tag`
- [x] `MessageType`
- [x] `TagSuggestion`
- [x] `select_message_content()`
- [ ] Error types
- All errors are currently raised as Python `ValueError` values that contain the full detail of the error.
## Build
To build, first set up a Python venv and install [maturin](https://github.com/PyO3/maturin):
```
python -m venv .venv
.\.venv\Scripts\activate
pip install maturin
maturin develop
```
Then build the library:
```
.\.venv\Scripts\activate
maturin develop
```
Then import it in Python:
```
py
> import loot
```
+16
View File
@@ -0,0 +1,16 @@
[build-system]
requires = ["maturin>=1.8,<2.0"]
build-backend = "maturin"
[project]
name = "libloot"
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"]
+347
View File
@@ -0,0 +1,347 @@
use std::{
hash::{DefaultHasher, Hash, Hasher},
path::PathBuf,
sync::{Arc, RwLock},
};
use libloot::error::DatabaseLockPoisonError;
use pyo3::{
Bound, PyResult, pyclass, pymethods,
types::{PyAnyMethods, PyTypeMethods},
};
use crate::{
UnsupportedEnumValueError, VerboseError,
metadata::{Group, Message, NONE_REPR, PluginMetadata},
};
#[pyclass]
#[derive(Clone, Debug)]
pub struct Database(Arc<RwLock<libloot::Database>>);
#[pymethods]
impl Database {
pub fn load_masterlist(&self, path: PathBuf) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_masterlist(&path)
.map_err(Into::into)
}
pub fn load_masterlist_with_prelude(
&self,
masterlist_path: PathBuf,
prelude_path: PathBuf,
) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_masterlist_with_prelude(&masterlist_path, &prelude_path)
.map_err(Into::into)
}
pub fn load_userlist(&self, path: PathBuf) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_userlist(&path)
.map_err(Into::into)
}
pub fn write_user_metadata(
&self,
output_path: PathBuf,
overwrite: bool,
) -> Result<(), VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.write_user_metadata(&output_path, overwrite)
.map_err(Into::into)
}
pub fn write_minimal_list(
&self,
output_path: PathBuf,
overwrite: bool,
) -> Result<(), VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.write_minimal_list(&output_path, overwrite)
.map_err(Into::into)
}
pub fn known_bash_tags(&self) -> Result<Vec<String>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.known_bash_tags())
}
pub fn general_messages(
&self,
evaluate_conditions: bool,
) -> Result<Vec<Message>, VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.general_messages(evaluate_conditions)
.map(|v| v.into_iter().map(Into::into).collect())
.map_err(Into::into)
}
pub fn groups(&self, include_user_metadata: bool) -> Result<Vec<Group>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.groups(include_user_metadata)
.into_iter()
.map(Into::into)
.collect())
}
fn user_groups(&self) -> Result<Vec<Group>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.user_groups()
.iter()
.cloned()
.map(Into::into)
.collect())
}
pub fn set_user_groups(&self, groups: Vec<Group>) -> Result<(), VerboseError> {
let groups = groups.into_iter().map(Into::into).collect();
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.set_user_groups(groups);
Ok(())
}
pub fn groups_path(
&self,
from_group_name: &str,
to_group_name: &str,
) -> Result<Vec<Vertex>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.groups_path(from_group_name, to_group_name)
.map(|v| v.into_iter().map(Into::into).collect())
.map_err(Into::into)
}
pub fn plugin_metadata(
&self,
plugin_name: &str,
include_user_metadata: bool,
evaluate_conditions: bool,
) -> Result<Option<PluginMetadata>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.plugin_metadata(plugin_name, include_user_metadata, evaluate_conditions)
.map(|p| p.map(Into::into))
.map_err(Into::into)
}
pub fn plugin_user_metadata(
&self,
plugin_name: &str,
evaluate_conditions: bool,
) -> Result<Option<PluginMetadata>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.plugin_user_metadata(plugin_name, evaluate_conditions)
.map(|p| p.map(Into::into))
.map_err(Into::into)
}
pub fn set_plugin_user_metadata(
&mut self,
plugin_metadata: PluginMetadata,
) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.set_plugin_user_metadata(plugin_metadata.into());
Ok(())
}
pub fn discard_plugin_user_metadata(&self, plugin: &str) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.discard_plugin_user_metadata(plugin);
Ok(())
}
pub fn discard_all_user_metadata(&self) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.discard_all_user_metadata();
Ok(())
}
}
impl From<Arc<RwLock<libloot::Database>>> for Database {
fn from(value: Arc<RwLock<libloot::Database>>) -> Self {
Self(value)
}
}
#[pyclass(eq, ord, str = "{0:?}")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(transparent)]
pub struct Vertex(libloot::Vertex);
#[pymethods]
impl Vertex {
#[new]
fn new(name: String) -> Self {
Self(libloot::Vertex::new(name))
}
#[getter]
fn name(&self) -> &str {
self.0.name()
}
#[getter]
fn out_edge_type(&self) -> Result<Option<EdgeType>, UnsupportedEnumValueError> {
self.0.out_edge_type().map(|e| e.try_into()).transpose()
}
#[setter]
fn set_out_edge_type(
&mut self,
out_edge_type: EdgeType,
) -> Result<(), UnsupportedEnumValueError> {
let out_edge_type = out_edge_type.try_into()?;
self.0.set_out_edge_type(out_edge_type);
Ok(())
}
fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
let class_name = slf.get_type().qualname()?;
let inner = &slf.borrow().0;
Ok(format!(
"{}({}, {})",
class_name,
inner.name(),
inner
.out_edge_type()
.map(repr_edge_type)
.unwrap_or(NONE_REPR),
))
}
fn __hash__(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
impl From<libloot::Vertex> for Vertex {
fn from(value: libloot::Vertex) -> Self {
Self(value)
}
}
impl From<Vertex> for libloot::Vertex {
fn from(value: Vertex) -> Self {
value.0
}
}
#[pyclass(eq, frozen, hash, ord)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum EdgeType {
Hardcoded,
MasterFlag,
Master,
MasterlistRequirement,
UserRequirement,
MasterlistLoadAfter,
UserLoadAfter,
MasterlistGroup,
UserGroup,
RecordOverlap,
AssetOverlap,
TieBreak,
BlueprintMaster,
}
impl TryFrom<libloot::EdgeType> for EdgeType {
type Error = UnsupportedEnumValueError;
fn try_from(value: libloot::EdgeType) -> Result<Self, Self::Error> {
match value {
libloot::EdgeType::Hardcoded => Ok(EdgeType::Hardcoded),
libloot::EdgeType::MasterFlag => Ok(EdgeType::MasterFlag),
libloot::EdgeType::Master => Ok(EdgeType::Master),
libloot::EdgeType::MasterlistRequirement => Ok(EdgeType::MasterlistRequirement),
libloot::EdgeType::UserRequirement => Ok(EdgeType::UserRequirement),
libloot::EdgeType::MasterlistLoadAfter => Ok(EdgeType::MasterlistLoadAfter),
libloot::EdgeType::UserLoadAfter => Ok(EdgeType::UserLoadAfter),
libloot::EdgeType::MasterlistGroup => Ok(EdgeType::MasterlistGroup),
libloot::EdgeType::UserGroup => Ok(EdgeType::UserGroup),
libloot::EdgeType::RecordOverlap => Ok(EdgeType::RecordOverlap),
libloot::EdgeType::AssetOverlap => Ok(EdgeType::AssetOverlap),
libloot::EdgeType::TieBreak => Ok(EdgeType::TieBreak),
libloot::EdgeType::BlueprintMaster => Ok(EdgeType::BlueprintMaster),
_ => Err(UnsupportedEnumValueError),
}
}
}
impl TryFrom<EdgeType> for libloot::EdgeType {
type Error = UnsupportedEnumValueError;
fn try_from(value: EdgeType) -> Result<Self, Self::Error> {
match value {
EdgeType::Hardcoded => Ok(libloot::EdgeType::Hardcoded),
EdgeType::MasterFlag => Ok(libloot::EdgeType::MasterFlag),
EdgeType::Master => Ok(libloot::EdgeType::Master),
EdgeType::MasterlistRequirement => Ok(libloot::EdgeType::MasterlistRequirement),
EdgeType::UserRequirement => Ok(libloot::EdgeType::UserRequirement),
EdgeType::MasterlistLoadAfter => Ok(libloot::EdgeType::MasterlistLoadAfter),
EdgeType::UserLoadAfter => Ok(libloot::EdgeType::UserLoadAfter),
EdgeType::MasterlistGroup => Ok(libloot::EdgeType::MasterlistGroup),
EdgeType::UserGroup => Ok(libloot::EdgeType::UserGroup),
EdgeType::RecordOverlap => Ok(libloot::EdgeType::RecordOverlap),
EdgeType::AssetOverlap => Ok(libloot::EdgeType::AssetOverlap),
EdgeType::TieBreak => Ok(libloot::EdgeType::TieBreak),
EdgeType::BlueprintMaster => Ok(libloot::EdgeType::BlueprintMaster),
}
}
}
fn repr_edge_type(value: libloot::EdgeType) -> &'static str {
match value {
libloot::EdgeType::Hardcoded => "EdgeType.Hardcoded",
libloot::EdgeType::MasterFlag => "EdgeType.MasterFlag",
libloot::EdgeType::Master => "EdgeType.Master",
libloot::EdgeType::MasterlistRequirement => "EdgeType.MasterlistRequirement",
libloot::EdgeType::UserRequirement => "EdgeType.UserRequirement",
libloot::EdgeType::MasterlistLoadAfter => "EdgeType.MasterlistLoadAfter",
libloot::EdgeType::UserLoadAfter => "EdgeType.UserLoadAfter",
libloot::EdgeType::MasterlistGroup => "EdgeType.MasterlistGroup",
libloot::EdgeType::UserGroup => "EdgeType.UserGroup",
libloot::EdgeType::RecordOverlap => "EdgeType.RecordOverlap",
libloot::EdgeType::AssetOverlap => "EdgeType.AssetOverlap",
libloot::EdgeType::TieBreak => "EdgeType.TieBreak",
libloot::EdgeType::BlueprintMaster => "EdgeType.BlueprintMaster",
_ => "<unknown EdgeType>",
}
}
+170
View File
@@ -0,0 +1,170 @@
use std::path::{Path, PathBuf};
use pyo3::{pyclass, pymethods};
use crate::{UnsupportedEnumValueError, VerboseError, database::Database, plugin::Plugin};
#[allow(non_camel_case_types)]
#[pyclass(eq, frozen, hash, ord)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum GameType {
tes4,
tes5,
fo3,
fonv,
fo4,
tes5se,
fo4vr,
tes5vr,
tes3,
starfield,
openmw,
}
impl TryFrom<libloot::GameType> for GameType {
type Error = UnsupportedEnumValueError;
fn try_from(value: libloot::GameType) -> Result<Self, Self::Error> {
match value {
libloot::GameType::TES4 => Ok(GameType::tes4),
libloot::GameType::TES5 => Ok(GameType::tes5),
libloot::GameType::FO3 => Ok(GameType::fo3),
libloot::GameType::FONV => Ok(GameType::fonv),
libloot::GameType::FO4 => Ok(GameType::fo4),
libloot::GameType::TES5SE => Ok(GameType::tes5se),
libloot::GameType::FO4VR => Ok(GameType::fo4vr),
libloot::GameType::TES5VR => Ok(GameType::tes5vr),
libloot::GameType::TES3 => Ok(GameType::tes3),
libloot::GameType::Starfield => Ok(GameType::starfield),
libloot::GameType::OpenMW => Ok(GameType::openmw),
_ => Err(UnsupportedEnumValueError),
}
}
}
impl TryFrom<GameType> for libloot::GameType {
type Error = UnsupportedEnumValueError;
fn try_from(value: GameType) -> Result<Self, Self::Error> {
match value {
GameType::tes4 => Ok(libloot::GameType::TES4),
GameType::tes5 => Ok(libloot::GameType::TES5),
GameType::fo3 => Ok(libloot::GameType::FO3),
GameType::fonv => Ok(libloot::GameType::FONV),
GameType::fo4 => Ok(libloot::GameType::FO4),
GameType::tes5se => Ok(libloot::GameType::TES5SE),
GameType::fo4vr => Ok(libloot::GameType::FO4VR),
GameType::tes5vr => Ok(libloot::GameType::TES5VR),
GameType::tes3 => Ok(libloot::GameType::TES3),
GameType::starfield => Ok(libloot::GameType::Starfield),
GameType::openmw => Ok(libloot::GameType::OpenMW),
}
}
}
#[pyclass]
#[derive(Debug)]
pub struct Game(libloot::Game);
#[pymethods]
impl Game {
#[new]
#[pyo3(signature = (game_type, game_path, local_path = None))]
fn new(
game_type: GameType,
game_path: PathBuf,
local_path: Option<PathBuf>,
) -> Result<Self, VerboseError> {
match local_path {
Some(local_path) => Ok(Game(libloot::Game::with_local_path(
game_type.try_into()?,
&game_path,
&local_path,
)?)),
None => Ok(Game(libloot::Game::new(game_type.try_into()?, &game_path)?)),
}
}
fn game_type(&self) -> Result<GameType, VerboseError> {
self.0.game_type().try_into().map_err(Into::into)
}
fn additional_data_paths(&self) -> &[PathBuf] {
self.0.additional_data_paths()
}
fn set_additional_data_paths(&mut self, paths: Vec<PathBuf>) -> Result<(), VerboseError> {
self.0.set_additional_data_paths(&as_paths(&paths))?;
Ok(())
}
fn database(&self) -> Database {
self.0.database().into()
}
fn is_valid_plugin(&self, plugin_path: PathBuf) -> bool {
self.0.is_valid_plugin(&plugin_path)
}
fn load_plugins(&mut self, plugin_paths: Vec<PathBuf>) -> Result<(), VerboseError> {
self.0.load_plugins(&as_paths(&plugin_paths))?;
Ok(())
}
fn load_plugin_headers(&mut self, plugin_paths: Vec<PathBuf>) -> Result<(), VerboseError> {
self.0.load_plugin_headers(&as_paths(&plugin_paths))?;
Ok(())
}
fn clear_loaded_plugins(&mut self) {
self.0.clear_loaded_plugins();
}
// TODO: Game::plugin().
// fn plugin(&self, plugin_name: &str) -> Option<&Plugin> {
// self.0.plugin(plugin_name).map(Plugin::wrap)
// }
// TODO: Game::loaded_plugins().
// fn loaded_plugins(&self) -> Vec<&Plugin> {
// wrap_plugins(self.0.loaded_plugins())
// }
fn sort_plugins(&self, plugin_names: Vec<String>) -> Result<Vec<String>, VerboseError> {
Ok(self.0.sort_plugins(&as_strs(&plugin_names))?)
}
fn load_current_load_order_state(&mut self) -> Result<(), VerboseError> {
self.0.load_current_load_order_state()?;
Ok(())
}
fn is_load_order_ambiguous(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_load_order_ambiguous()?)
}
fn active_plugins_file_path(&self) -> &PathBuf {
self.0.active_plugins_file_path()
}
fn is_plugin_active(&self, plugin_name: &str) -> bool {
self.0.is_plugin_active(plugin_name)
}
fn load_order(&self) -> Vec<&str> {
self.0.load_order()
}
fn set_load_order(&mut self, load_order: Vec<String>) -> Result<(), VerboseError> {
self.0.set_load_order(&as_strs(&load_order))?;
Ok(())
}
}
fn as_paths(pathbufs: &[PathBuf]) -> Vec<&Path> {
pathbufs.iter().map(PathBuf::as_ref).collect()
}
fn as_strs(strings: &[String]) -> Vec<&str> {
strings.iter().map(String::as_ref).collect()
}
+108
View File
@@ -0,0 +1,108 @@
mod database;
mod game;
mod metadata;
mod plugin;
use database::{Database, EdgeType, Vertex};
use game::{Game, GameType};
use metadata::{
File, Filename, Group, Location, Message, MessageContent, MessageType, PluginMetadata, Tag,
TagSuggestion, select_message_content,
};
use plugin::Plugin;
use pyo3::{exceptions::PyValueError, prelude::*};
#[pyfunction]
fn is_compatible(major: u32, minor: u32, patch: u32) -> bool {
libloot::is_compatible(major, minor, patch)
}
#[pyfunction]
fn libloot_revision() -> String {
libloot::libloot_revision()
}
#[pyfunction]
fn libloot_version() -> String {
libloot::libloot_version()
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct UnsupportedEnumValueError;
impl std::fmt::Display for UnsupportedEnumValueError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Enum value is unsupported")
}
}
impl std::error::Error for UnsupportedEnumValueError {}
impl From<UnsupportedEnumValueError> for PyErr {
fn from(value: UnsupportedEnumValueError) -> Self {
PyValueError::new_err(value.to_string())
}
}
#[derive(Debug)]
pub struct VerboseError(Box<dyn std::error::Error>);
impl std::fmt::Display for VerboseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let e = &self.0;
write!(f, "{}", e)?;
let mut error = e.as_ref();
while let Some(source) = error.source() {
write!(f, ": {}", source)?;
error = source;
}
Ok(())
}
}
// impl std::error::Error for VerboseError {}
impl<T: std::error::Error + 'static> From<T> for VerboseError {
fn from(value: T) -> Self {
VerboseError(Box::new(value))
}
}
impl From<VerboseError> for PyErr {
fn from(value: VerboseError) -> Self {
PyValueError::new_err(value.to_string())
}
}
/// A Python module implemented in Rust.
#[pymodule(name = "loot")]
fn libloot_pyo3(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("LIBLOOT_VERSION_MAJOR", libloot::LIBLOOT_VERSION_MAJOR)?;
m.add("LIBLOOT_VERSION_MINOR", libloot::LIBLOOT_VERSION_MINOR)?;
m.add("LIBLOOT_VERSION_PATCH", libloot::LIBLOOT_VERSION_PATCH)?;
m.add_function(wrap_pyfunction!(is_compatible, m)?)?;
m.add_function(wrap_pyfunction!(libloot_revision, m)?)?;
m.add_function(wrap_pyfunction!(libloot_version, m)?)?;
m.add_function(wrap_pyfunction!(select_message_content, m)?)?;
m.add_class::<Vertex>()?;
m.add_class::<EdgeType>()?;
m.add_class::<GameType>()?;
m.add_class::<Game>()?;
m.add_class::<Database>()?;
m.add_class::<Plugin>()?;
m.add_class::<Group>()?;
m.add_class::<MessageContent>()?;
m.add_class::<MessageType>()?;
m.add_class::<Message>()?;
m.add_class::<File>()?;
m.add_class::<Filename>()?;
m.add_class::<Tag>()?;
m.add_class::<TagSuggestion>()?;
m.add_class::<Location>()?;
m.add_class::<PluginMetadata>()?;
Ok(())
}
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
use pyo3::{pyclass, pymethods};
use crate::VerboseError;
#[pyclass(eq, frozen)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct Plugin(libloot::Plugin);
#[pymethods]
impl Plugin {
fn name(&self) -> &str {
self.0.name()
}
fn header_version(&self) -> Option<f32> {
self.0.header_version()
}
fn version(&self) -> Option<&str> {
self.0.version()
}
fn masters(&self) -> Result<Vec<String>, VerboseError> {
Ok(self.0.masters()?)
}
fn bash_tags(&self) -> &[String] {
self.0.bash_tags()
}
fn crc(&self) -> Option<u32> {
self.0.crc()
}
fn is_master(&self) -> bool {
self.0.is_master()
}
fn is_light_plugin(&self) -> bool {
self.0.is_light_plugin()
}
fn is_medium_plugin(&self) -> bool {
self.0.is_medium_plugin()
}
fn is_update_plugin(&self) -> bool {
self.0.is_update_plugin()
}
fn is_blueprint_plugin(&self) -> bool {
self.0.is_blueprint_plugin()
}
fn is_valid_as_light_plugin(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_valid_as_light_plugin()?)
}
fn is_valid_as_medium_plugin(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_valid_as_medium_plugin()?)
}
fn is_valid_as_update_plugin(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_valid_as_update_plugin()?)
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn loads_archive(&self) -> bool {
self.0.loads_archive()
}
fn do_records_overlap(&self, plugin: &Self) -> Result<bool, VerboseError> {
Ok(self.0.do_records_overlap(&plugin.0)?)
}
}
impl Plugin {
pub fn wrap(plugin: &libloot::Plugin) -> &Plugin {
let p = plugin as *const libloot::Plugin;
// SAFETY: Plugin is a transparent wrapper, so reinterpreting the pointer is safe.
unsafe {
let p = p as *const Plugin;
&*p
}
}
}
pub fn wrap_plugins(mut vec: Vec<&libloot::Plugin>) -> Vec<&Plugin> {
// SAFETY: This is safe because Plugin is a transparent wrapper around the libloot type.
unsafe { Vec::from_raw_parts(vec.as_mut_ptr().cast(), vec.len(), vec.capacity()) }
}