Author SHA1 Message Date
Oliver Hamlet 01a8567ed8 Move most reference docs into source as docstrings
It's easier to maintain, though there seems to be a bug in pybind11, as
it doesn't use the docstrings I've provided for some static read-only
properties, so Version and WrapperVersion are still documented
separately.

It's not related, but I've noticed that for some reason intersphinx
is unable to turn :cpp: domain references into hyperlinks, but no
related information is logged to say why.
2020-01-04 16:07:36 +00:00
Oliver Hamlet 7ccfe6d7f4 Expose a few more GameInterface and PluginInterface methods
The exposed methods are:

* GameInterface::LoadPlugins
* GameInterface::GetPlugin
* PluginInterface::GetName
* PluginInterface::IsMaster
* PluginInterface::IsLightMaster
* PluginInterface::IsValidAsLightMaster
2020-01-04 16:07:36 +00:00
Oliver Hamlet 2106ab4f01 Fix Python GIL causing a hang when running the logging callback
pybind11 holds the GIL whenever a C++ function is called from Python, so
the logging callback can't then execute Python code. To avoid this
problem, release the GIL for every function bound by pybind11 (in case
log statements are added to those that don't currently have any), and
wrap the logging callback in a lambda function that first aquires the
GIL lock for the function's scope.
2020-01-04 12:37:00 +00:00
12 changed files with 340 additions and 459 deletions
-88
View File
@@ -1,88 +0,0 @@
name: CI
on:
push:
# Don't run this workflow when a tag is pushed.
branches:
- '*'
pull_request:
env:
MSVC_CONFIG: RelWithDebInfo
jobs:
windows:
runs-on: windows-2025
strategy:
matrix:
platform: [Win32, x64]
python-version: [3.7]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Python architecture
id: get-python-architecture
shell: bash
run: |
if [[ "${{ matrix.platform }}" == "Win32" ]]
then
PLATFORM=x86
else
PLATFORM=x64
fi
echo "architecture=$PLATFORM" >> $GITHUB_OUTPUT
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ steps.get-python-architecture.outputs.architecture }}
- name: Get descriptive version
id: get-version
shell: bash
run: |
GIT_DESCRIBE=$(git describe --tags --long --abbrev=7)
DESC_REF=${GIT_DESCRIBE}_${GITHUB_REF#refs/*/}
SAFE_DESC_REF=${DESC_REF//[\/<>\"|]/_}
echo "version=$SAFE_DESC_REF" >> $GITHUB_OUTPUT
- name: Run CMake
run: |
mkdir build
cd build
cmake .. -G "Visual Studio 17 2022" -A ${{ matrix.platform }} -DCPACK_PACKAGE_VERSION="${{ steps.get-version.outputs.version }}-python${{ matrix.python-version }}"
cmake --build . --config ${{ env.MSVC_CONFIG }}
- name: Run tests
run: |
cd build
ctest -C ${{ env.MSVC_CONFIG }}
- name: Build archive
id: build-archive
shell: bash
run: |
cd build
cpack -C ${{ env.MSVC_CONFIG }}
VERSION="${{ steps.get-version.outputs.version }}-python${{ matrix.python-version }}"
if [[ "${{ matrix.platform }}" == "Win32" ]]
then
PLATFORM=win32
else
PLATFORM=win64
fi
echo "filename=libloot-python-${VERSION}-${PLATFORM}.zip" >> $GITHUB_OUTPUT
- name: Upload archive
uses: actions/upload-artifact@v4
with:
name: ${{ steps.build-archive.outputs.filename }}
path: build/package/${{ steps.build-archive.outputs.filename }}
if: github.event_name == 'push'
-92
View File
@@ -1,92 +0,0 @@
name: Release
on:
push:
tags: '*'
env:
MSVC_CONFIG: RelWithDebInfo
jobs:
create_release:
runs-on: ubuntu-18.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
git_tag: ${{ steps.get-git-tag.outputs.name }}
steps:
- name: Get Git tag
id: get-git-tag
run: echo "name=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get-git-tag.outputs.name }}
release_name: libloot-python v${{ steps.get-git-tag.outputs.name }}
body: |
Requires Windows 7 or later and the [MSVC 2022 x86 redistributable](https://aka.ms/vs/17/release/vc_redist.x86.exe), and [7-Zip](http://www.7-zip.org/) to extract the archive.
windows:
runs-on: windows-2025
needs: create_release
strategy:
matrix:
platform: [Win32, x64]
python-version: [3.7]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Get descriptive version
id: get-version
shell: bash
run: |
GIT_DESCRIBE=$(git describe --tags --long --abbrev=7)
DESC_REF=${GIT_DESCRIBE}_${GITHUB_REF#refs/*/}
SAFE_DESC_REF=${DESC_REF//[\/<>\"|]/_}
echo "version=$SAFE_DESC_REF" >> $GITHUB_OUTPUT
- name: Run CMake
run: |
mkdir build
cd build
cmake .. -G "Visual Studio 17 2022" -A ${{ matrix.platform }} -DCPACK_PACKAGE_VERSION="${{ steps.get-version.outputs.version }}-python${{ matrix.python-version }}"
cmake --build . --config ${{ env.MSVC_CONFIG }}
- name: Build archive
id: build-archive
shell: bash
run: |
cd build
cpack -C ${{ env.MSVC_CONFIG }}
VERSION="${{ steps.get-version.outputs.version }}-python${{ matrix.python-version }}"
if [[ "${{ matrix.platform }}" == "Win32" ]]
then
PLATFORM=win32
else
PLATFORM=win64
fi
echo "filename=libloot-python-${VERSION}-${PLATFORM}.zip" >> $GITHUB_OUTPUT
- name: Upload Archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_path: build/package/${{ steps.build-archive.outputs.filename }}
asset_name: ${{ steps.build-archive.outputs.filename }}
asset_content_type: application/x-7z-compressed
+39 -15
View File
@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.4)
cmake_minimum_required (VERSION 3.1)
project (libloot-python)
include(ExternalProject)
@@ -40,7 +40,7 @@ configure_file("${CMAKE_SOURCE_DIR}/python/setup.py" "${CMAKE_BINARY_DIR}/genera
# pybind11
#######################################
set(PYBIND11_VERSION "2.9.2")
set(PYBIND11_VERSION "2.4.2")
set(PYBIND11_URL "https://github.com/pybind/pybind11/archive/v${PYBIND11_VERSION}.tar.gz")
set(PYBIND11_DOWNLOAD_PATH "${EXTERNAL_PROJECTS_PATH}/pybind11-${PYBIND11_VERSION}.tar.gz")
set(PYBIND11_EXTRACTED_PATH "${EXTERNAL_PROJECTS_PATH}/pybind11-${PYBIND11_VERSION}")
@@ -84,6 +84,20 @@ link_directories(${LIBLOOT_EXTRACTED_PATH})
set(LIBLOOT_STATIC_LIBRARY "${CMAKE_STATIC_LIBRARY_PREFIX}loot${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(LIBLOOT_SHARED_LIBRARY "${CMAKE_SHARED_LIBRARY_PREFIX}loot${CMAKE_SHARED_LIBRARY_SUFFIX}")
#######################################
# testing-plugins
#######################################
# This is a bit messy because the testing-plugins aren't actually required to
# build libloot-python, but having them as a dependency is the easiest way to
# manage downloading and moving them to the expected location.
ExternalProject_Add(testing-plugins
PREFIX "external"
URL "https://github.com/Ortham/testing-plugins/archive/1.4.1.tar.gz"
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND "")
#######################################
# Python Module
#######################################
@@ -96,7 +110,7 @@ pybind11_add_module(libloot-python "${CMAKE_SOURCE_DIR}/src/main.cpp"
# conventions (no lib, no -python).
set_target_properties(libloot-python PROPERTIES OUTPUT_NAME loot)
add_dependencies(libloot-python libloot)
add_dependencies(libloot-python libloot testing-plugins)
if (CMAKE_SYSTEM_NAME MATCHES "Windows")
target_link_libraries(libloot-python PRIVATE ${LIBLOOT_STATIC_LIBRARY})
@@ -116,6 +130,13 @@ add_custom_command(TARGET libloot-python POST_BUILD
"${LIBLOOT_EXTRACTED_PATH}/${LIBLOOT_SHARED_LIBRARY}"
"$<TARGET_FILE_DIR:libloot-python>/${LIBLOOT_SHARED_LIBRARY}")
# Copy testing plugins
ExternalProject_Get_Property(testing-plugins SOURCE_DIR)
add_custom_command(TARGET libloot-python POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${SOURCE_DIR}
$<TARGET_FILE_DIR:libloot-python>)
enable_testing()
add_test(NAME python
@@ -156,22 +177,25 @@ ENDIF ()
# Get version info using Git if available
find_package(Git)
IF (NOT DEFINED CPACK_PACKAGE_VERSION)
IF (GIT_FOUND)
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --long --always --abbrev=7
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_DESCRIBE_STRING
OUTPUT_STRIP_TRAILING_WHITESPACE)
IF (GIT_FOUND)
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --long --always
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_DESCRIBE_STRING
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(REPLACE "/" "-" GIT_DESCRIBE_STRING ${GIT_DESCRIBE_STRING})
ELSE()
SET (GIT_DESCRIBE_STRING "unknown-version")
ENDIF ()
IF (DEFINED ENV{APPVEYOR_REPO_BRANCH})
set(GIT_DESCRIBE_STRING "${GIT_DESCRIBE_STRING}_$ENV{APPVEYOR_REPO_BRANCH}")
ELSEIF (DEFINED ENV{TRAVIS_BRANCH})
set(GIT_DESCRIBE_STRING "${GIT_DESCRIBE_STRING}_$ENV{TRAVIS_BRANCH}")
ENDIF()
set(CPACK_PACKAGE_VERSION "${GIT_DESCRIBE_STRING}-python$ENV{PYTHON_VERSION}")
ENDIF()
string(REPLACE "/" "-" GIT_DESCRIBE_STRING ${GIT_DESCRIBE_STRING})
ELSE()
SET (GIT_DESCRIBE_STRING "unknown-version")
ENDIF ()
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_VERSION "${GIT_DESCRIBE_STRING}-python$ENV{PYTHON_VERSION}")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/package")
include(CPack)
+3 -7
View File
@@ -1,24 +1,20 @@
libloot-python
==============
**This repository is no longer maintained. A more complete and up to date Python wrapper can be found in the [libloot repository](https://github.com/loot/libloot/tree/aaacebefa053a6b7504bf61a73d2783a5436ca25/python).**
![CI](https://github.com/loot/libloot-python/workflows/CI/badge.svg?branch=master&event=push)
[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/k2ugge3po7254o1o/branch/master?svg=true)](https://ci.appveyor.com/project/LOOT/libloot-python/branch/master)
[![Documentation Status](https://readthedocs.org/projects/loot-api-python/badge/)](http://loot-api-python.readthedocs.io/)
A Python module that wraps libloot, generated by [pybind11](https://github.com/pybind/pybind11). Not everything in the API is exposed: coverage can be extended on request (or by pull request).
## Downloads
Releases are hosted on [GitHub](https://github.com/loot/libloot-python/releases).
Snapshot builds are available as artifacts from [GitHub Actions runs](https://github.com/loot/libloot-python/actions), though they are only kept for 90 days and can only be downloaded when logged into a GitHub account. The snapshot build archives are named like so:
Snapshot builds are available on [Bintray](https://bintray.com/loot/snapshots/libloot-python). The snapshot build archives are named like so:
```
libloot-python-<short revision ID>-python<python version>-win<architecture>.zip
```
For example `libloot-python-94de368-python3.7-win32.zip` was built using the revision with shortened commit ID `94de368`.
For example `libloot-python-94de368-python2.7-win32.zip` was built using the revision with shortened commit ID `94de368`.
## Documentation
+83
View File
@@ -0,0 +1,83 @@
os: Visual Studio 2017
version: "{build}-{branch}"
configuration: RelWithDebInfo
platform:
- Win32
- x64
environment:
bintray_auth_token:
secure: PgsEA6TjHVf718zMnK7J/fT1hUAVNKBeWhpYgYaeCyeZk37VT4Ics6j7+B7ElLEr
github_auth_token:
secure: yDqT5l/e5MntbW99V6+MHlfFgNv+UIogFfeyUVqtFk5lFRB/dAraLLwKCLl6y+DH
matrix:
- PYTHON_VERSION: 2.7
- PYTHON_VERSION: 3.7
install:
- ps: (New-Object Net.WebClient).DownloadFile('https://raw.githubusercontent.com/Ortham/ci-scripts/2.0.0/delete_old_bintray_versions.py', "$env:APPVEYOR_BUILD_FOLDER\delete_old_bintray_versions.py")
- ps: (New-Object Net.WebClient).DownloadFile('https://github.com/WrinklyNinja/testing-plugins/archive/1.4.1.zip', "$PWD/1.4.1.zip")
- 7z x 1.4.1.zip
- mv testing-plugins-1.4.1 testing-plugins
before_build:
- ps: $env:PYTHON_DIRECTORY_VERSION=$env:PYTHON_VERSION.Replace(".", "")
- cd %APPVEYOR_BUILD_FOLDER%
- ps: mkdir build
- cd build
- ps: |
if ($env:PLATFORM -eq 'Win32') {
cmake .. -G "Visual Studio 15 2017" -DPYTHON_EXECUTABLE="C:\Python${env:PYTHON_DIRECTORY_VERSION}\python.exe"
} else {
cmake .. -G "Visual Studio 15 2017" -A x64 -DPYTHON_EXECUTABLE="C:\Python${env:PYTHON_DIRECTORY_VERSION}-x64\python.exe"
}
build:
verbosity: minimal
project: 'c:\projects\libloot-python\build\libloot-python.sln'
test_script:
- cd %APPVEYOR_BUILD_FOLDER%\build
- ctest -C %CONFIGURATION%
after_test:
- cd %APPVEYOR_BUILD_FOLDER%\build
- cpack -C %CONFIGURATION%
- ps: $env:GIT_DESCRIBE = ((git describe --tags --long --always) | Out-String) -replace "`n|`r", ""
- ps: $env:PACKAGE_VERSION = $env:GIT_DESCRIBE + "_" + ($env:APPVEYOR_REPO_BRANCH.replace("/", "-"))
- ps: $env:ARCHITECTURE = $env:PLATFORM.substring($env:PLATFORM.length - 2)
artifacts:
- path: build\package\libloot-python-$(PACKAGE_VERSION)-python$(PYTHON_VERSION)-win$(ARCHITECTURE).zip
name: libloot-python
deploy:
- provider: BinTray
username: wrinklyninja
api_key:
secure: PgsEA6TjHVf718zMnK7J/fT1hUAVNKBeWhpYgYaeCyeZk37VT4Ics6j7+B7ElLEr
subject: loot
repo: snapshots
package: libloot-python
version: $(PACKAGE_VERSION)
publish: true
artifact: libloot-python
- provider: GitHub
tag: $(APPVEYOR_REPO_TAG_NAME)
release: libloot-python v$(APPVEYOR_REPO_TAG_NAME)
description: |
Requires Windows 7 or later and the [MSVC 2017 x86 redistributable](https://download.visualstudio.microsoft.com/download/pr/749aa419-f9e4-4578-a417-a43786af205e/d59197078cc425377be301faba7dd87a/vc_redist.x86.exe), and [7-Zip](http://www.7-zip.org/) to extract the archive.
auth_token:
secure: yDqT5l/e5MntbW99V6+MHlfFgNv+UIogFfeyUVqtFk5lFRB/dAraLLwKCLl6y+DH
artifact: libloot-python
draft: false
on:
appveyor_repo_tag: true
on_success:
- ps: python "$env:APPVEYOR_BUILD_FOLDER\delete_old_bintray_versions.py" -g loot/libloot-python -b loot/snapshots/libloot-python -u wrinklyninja -k $env:bintray_auth_token -t $env:github_auth_token -n 30
+1 -1
View File
@@ -3,7 +3,7 @@ libloot-python
This archive contains a build of the libloot-python wrapper module and its corresponding 32-bit Windows build of libloot.
The DLL requires the [MSVC 2022 x86 redistributable](https://aka.ms/vs/17/release/vc_redist.x86.exe)
The DLL requires the [MSVC 2017 x86 redistributable](https://download.visualstudio.microsoft.com/download/pr/749aa419-f9e4-4578-a417-a43786af205e/d59197078cc425377be301faba7dd87a/vc_redist.x86.exe)
to be installed.
See the [online documentation](http://loot-api-python.readthedocs.org/) for more information.
+4 -2
View File
@@ -22,7 +22,7 @@
import os
import sys
sys.path.insert(0, os.path.abspath('../build/Release'))
sys.path.insert(0, os.path.abspath('../build/RelWithDebInfo'))
# -- General configuration ------------------------------------------------
@@ -34,6 +34,7 @@ sys.path.insert(0, os.path.abspath('../build/Release'))
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
]
@@ -346,5 +347,6 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'loot_api': ('http://loot.readthedocs.io/en/0.14.7/', None),
'loot': ('https://loot.readthedocs.io/en/0.15.0/', None),
'python': ('https://docs.python.org/3', None)
}
+7 -191
View File
@@ -5,188 +5,13 @@ API Reference
As this API is just a wrapper for libloot's C++ API, its documentation is linked
to for all non-Python-specific information.
Enumerations
============
The wrapped enumeration types below are classes in Python, but the distinction
The wrapped enumeration types are classes in Python, but the distinction
makes no difference in practice, so they're grouped here for semantics. All
values are unsigned integer constants.
their values are unsigned integer constants.
.. py:class:: loot.GameType
Wraps :cpp:type:`loot::GameType` to expose libloot's game codes.
.. py:attribute:: fo3
.. py:attribute:: fo4
.. py:attribute:: fonv
.. py:attribute:: tes4
.. py:attribute:: tes5
.. py:attribute:: tes5se
.. py:class:: loot.LogLevel
Wraps :cpp:type:`loot::LogLevel` to expose libloot's log level codes.
.. py:attribute:: trace
.. py:attribute:: debug
.. py:attribute:: info
.. py:attribute:: warning
.. py:attribute:: error
.. py:attribute:: fatal
.. py:class:: loot.MessageType
Wraps :cpp:type:`loot::MessageType` to expose libloot's message type
codes.
.. py:attribute:: error
.. py:attribute:: say
.. py:attribute:: warn
.. py:class:: loot.PluginCleanliness
Codes used to indicate the cleanliness of a plugin according to the
information contained within the loaded masterlist/userlist.
.. py:attribute:: clean
Indicates that the plugin is clean.
.. py:attribute:: dirty
Indicates that the plugin is dirty.
.. py:attribute:: do_not_clean
Indicates that the plugin contains dirty edits, but that they are part of
the plugin’s intended functionality and should not be removed.
.. py:attribute:: unknown
Indicates that no data is available on whether the plugin is dirty or not.
Public-Field Data Structures
============================
Classes with public fields and no member functions.
.. py:class:: loot.MasterlistInfo
Wraps :cpp:class:`loot::MasterlistInfo`.
.. py:attribute:: revision_id
A Unicode string containing a Git commit's SHA-1 checksum.
.. py:attribute:: revision_date
A Unicode string containing the date of the commit given by :py:attr:`revision_id`, in ISO 8601 format (YYYY-MM-DD).
.. py:attribute:: is_modified
A boolean that is true if the masterlist has been modified from its state
at the commit given by :py:attr:`revision_id`.
.. py:class:: loot.SimpleMessage
Wraps :cpp:class:`loot::SimpleMessage`.
.. py:attribute:: type
A :py:class:`loot.MessageType` giving the message type.
.. py:attribute:: language
A Unicode string giving the message text language.
.. py:attribute:: text
A Unicode string containing the message text.
.. py:attribute:: condition
A Unicode string containing the message condition.
.. py:class:: loot.PluginTags
Wraps :cpp:class:`loot::PluginTags`.
.. py:attribute:: added
A set of Unicode strings giving Bash Tags suggested for addition.
.. py:attribute:: removed
A set of Unicode strings giving Bash Tags suggested for removal.
.. py:attribute:: userlist_modified
A boolean that is true if the suggestions contain metadata obtained from a loaded userlist.
Functions
=========
.. py:function:: loot.set_logging_callback(callback) -> NoneType
Set the callback function that is called when logging. Wraps
:cpp:func:`loot::SetLoggingCallback`.
.. py:function:: loot.is_compatible(int, int, int) -> bool
Checks for API compatibility. Wraps :cpp:func:`loot::IsCompatible`.
.. py:function:: loot.create_game_handle(game : loot.GameType, game_path : unicode, [game_local_path : unicode = u'']) -> loot.GameInterface
Initialise a new game handle. Wraps :cpp:func:`loot::CreateGameHandle`.
Classes
=======
.. py:class:: loot.GameInterface
Wraps :cpp:class:`loot::GameInterface`.
.. py:function:: loot.get_database() -> loot.DatabaseInterface
Get a database handle. Wraps :cpp:func:`loot::GetDatabase`.
.. py:function:: loot.load_current_load_order_state() -> NoneType
Load the current load order state, discarding any previously held state.
Wraps :cpp:func:`loot::LoadCurrentLoadOrderState`.
.. py:class:: loot.DatabaseInterface
Wraps :cpp:class:`loot::DatabaseInterface`.
.. py:method:: get_masterlist_revision(loot.DatabaseInterface, unicode, bool) -> loot.MasterlistInfo
Gets the give masterlist’s source control revision. Wraps :cpp:func:`GetMasterlistRevision`.
.. py:method:: get_plugin_metadata(loot.DatabaseInterface, plugin : unicode, [includeUserMetadata : bool = True, [evaluateConditions : bool = False]]) -> loot.PluginMetadata
Get all a plugin’s loaded metadata. Wraps :cpp:func:`GetPluginMetadata`.
.. py:method:: get_plugin_cleanliness(loot.DatabaseInterface, plugin : unicode, [evaluateConditions : bool = False]) -> loot.PluginCleanliness
Determines the database’s knowledge of a plugin’s cleanliness. Outputs whether the plugin should be cleaned or not, or if no data is available.
.. py:method:: get_plugin_tags(loot.DatabaseInterface, plugin : unicode, [evaluateConditions : bool = False]) -> loot.PluginTags
Outputs the Bash Tags suggested for addition and removal by the database for the given plugin.
.. py:method:: load_lists(loot.DatabaseInterface, masterlist_path : unicode, [userlist_path : unicode = u'']) -> NoneType
Loads the masterlist and userlist from the paths specified. Wraps :cpp:func:`LoadLists`.
.. py:method:: update_masterlist(loot.DatabaseInterface, unicode, unicode, unicode) -> bool
Updates the given masterlist using the given Git repository details. Wraps :cpp:func:`UpdateMasterlist`.
.. py:method:: write_minimal_list(loot.DatabaseInterface, unicode, bool) -> NoneType
Writes a minimal metadata file containing only Bash Tag suggestions and/or cleanliness info from the loaded metadata. Wraps :cpp:func:`WriteMinimalList`.
.. automodule:: loot
:members:
:exclude-members: Version, WrapperVersion
.. py:class:: loot.Version
@@ -208,7 +33,7 @@ Classes
A Unicode string containing the SHA-1 of the Git revision that the wrapped C++ API was built from.
.. py:staticmethod:: string() -> unicode
.. py:staticmethod:: string() -> str
Returns the API version as a string of the form ``major.minor.patch``
@@ -232,15 +57,6 @@ Classes
A Unicode string containing the SHA-1 of the Git revision that the wrapped C++ API was built from.
.. py:staticmethod:: string() -> unicode
.. py:staticmethod:: string() -> str
Returns the API version as a string of the form ``major.minor.patch``
.. py:class:: loot.PluginMetadata
Wraps :cpp:class:`loot::PluginMetadata`.
.. py:method:: get_simple_messages(loot.PluginMetadata, unicode) -> list<loot.SimpleMessage>
Get the plugin’s messages as SimpleMessage objects for the given language.
Wraps :cpp:func:`GetPluginMessages`.
+2 -2
View File
@@ -10,13 +10,13 @@ Build archives contain two binaries:
* ``loot.*.pyd`` is the Python wrapper
* ``loot.dll`` is the C++ library DLL that the Python wrapper was built against.
The C++ DLL requires the `Visual C++ 2022 Redistributable (x86)`_
The C++ DLL requires the `Visual C++ 2017 Redistributable (x86)`_
to be installed.
To use the wrapper, copy both files to wherever you want to import them from
(they must be in the same folder), and you're done!
.. _Visual C++ 2022 Redistributable (x86): https://aka.ms/vs/17/release/vc_redist.x86.exe
.. _Visual C++ 2017 Redistributable (x86): https://download.visualstudio.microsoft.com/download/pr/749aa419-f9e4-4578-a417-a43786af205e/d59197078cc425377be301faba7dd87a/vc_redist.x86.exe
Using the wrapper
=================
+2 -1
View File
@@ -79,10 +79,11 @@ setup(
distclass=BinaryDistribution,
classifiers=[
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: C++'
'Operating System :: Microsoft :: Windows',
'Intended Audience :: Developers',
],
python_requires='>=3.7',
python_requires='>=2.7',
)
+137 -52
View File
@@ -58,10 +58,21 @@ bool UpdateMasterlist(std::shared_ptr<DatabaseInterface> db, std::string masterl
MasterlistInfo GetMasterlistRevision(std::shared_ptr<DatabaseInterface> db, std::string masterlistPath, bool getShortId) {
return db->GetMasterlistRevision(u8path(masterlistPath), getShortId);
}
void SetLoggingCallback(std::function<void(LogLevel, const char*)> callback) {
callback = [callback](LogLevel level, const char* message) {
pybind11::gil_scoped_acquire acquire;
callback(level, message);
};
loot::SetLoggingCallback(callback);
}
}
void bindEnums(pybind11::module& module) {
enum_<GameType>(module, "GameType")
pybind11::options options;
options.disable_function_signatures();
enum_<GameType>(module, "GameType", "Wraps :cpp:enum:`loot::GameType` to expose libloot's game codes.")
.value("tes4", GameType::tes4)
.value("tes5", GameType::tes5)
.value("tes5se", GameType::tes5se)
@@ -71,7 +82,7 @@ void bindEnums(pybind11::module& module) {
.value("fo4", GameType::fo4)
.value("fo4vr", GameType::fo4vr);
enum_<LogLevel>(module, "LogLevel")
enum_<LogLevel>(module, "LogLevel", "Wraps :cpp:enum:`loot::LogLevel` to expose libloot's log level codes.")
.value("trace", LogLevel::trace)
.value("debug", LogLevel::debug)
.value("info", LogLevel::info)
@@ -79,75 +90,140 @@ void bindEnums(pybind11::module& module) {
.value("error", LogLevel::error)
.value("fatal", LogLevel::fatal);
enum_<MessageType>(module, "MessageType")
enum_<MessageType>(module, "MessageType", "Wraps :cpp:enum:`loot::MessageType` to expose libloot's message type codes.")
.value("say", MessageType::say)
.value("warn", MessageType::warn)
.value("error", MessageType::error);
enum_<PluginCleanliness>(module, "PluginCleanliness")
.value("clean", PluginCleanliness::clean)
.value("dirty", PluginCleanliness::dirty)
.value("do_not_clean", PluginCleanliness::do_not_clean)
.value("unknown", PluginCleanliness::unknown);
enum_<PluginCleanliness>(module, "PluginCleanliness", "Codes used to indicate the cleanliness of a plugin according to the information contained within the loaded masterlist / userlist.")
.value("clean", PluginCleanliness::clean, "Indicates that the plugin is clean.")
.value("dirty", PluginCleanliness::dirty, "Indicates that the plugin is dirty.")
.value("do_not_clean", PluginCleanliness::do_not_clean, "Indicates that the plugin contains dirty edits, but that they are part of the plugin's intended functionality and should not be removed.")
.value("unknown", PluginCleanliness::unknown, "Indicates that no data is available on whether the plugin is dirty or not.");
}
void bindMetadataClasses(pybind11::module& module) {
class_<MasterlistInfo>(module, "MasterlistInfo")
.def_readwrite("revision_id", &MasterlistInfo::revision_id)
.def_readwrite("revision_date", &MasterlistInfo::revision_date)
.def_readwrite("is_modified", &MasterlistInfo::is_modified);
class_<MasterlistInfo>(module, "MasterlistInfo", "Wraps :cpp:class:`loot::MasterlistInfo`.")
.def_readwrite("revision_id", &MasterlistInfo::revision_id, "A Unicode string containing a Git commit's SHA-1 checksum.")
.def_readwrite("revision_date", &MasterlistInfo::revision_date, "A Unicode string containing the date of the commit given by :py:attr:`~loot.MasterlistInfo.revision_id`, in ISO 8601 format (YYYY-MM-DD).")
.def_readwrite("is_modified", &MasterlistInfo::is_modified, "A boolean that is true if the masterlist has been modified from its state at the commit given by :py:attr:`~loot.MasterlistInfo.revision_id`.");
class_<SimpleMessage>(module, "SimpleMessage")
.def_readwrite("type", &SimpleMessage::type)
.def_readwrite("language", &SimpleMessage::language)
.def_readwrite("text", &SimpleMessage::text)
.def_readwrite("condition", &SimpleMessage::condition);
class_<SimpleMessage>(module, "SimpleMessage", "Wraps :cpp:class:`loot::SimpleMessage`.")
.def_readwrite("type", &SimpleMessage::type, "A :py:class:`loot.MessageType` giving the message type.")
.def_readwrite("language", &SimpleMessage::language, "A Unicode string giving the message text language.")
.def_readwrite("text", &SimpleMessage::text, "A Unicode string containing the message text.")
.def_readwrite("condition", &SimpleMessage::condition, "A Unicode string containing the message condition.");
class_<PluginTags>(module, "PluginTags")
.def_readwrite("added", &PluginTags::added)
.def_readwrite("removed", &PluginTags::removed)
.def_readwrite("userlist_modified", &PluginTags::userlist_modified);
.def_readwrite("added", &PluginTags::added, "A set of Unicode strings giving Bash Tags suggested for addition.")
.def_readwrite("removed", &PluginTags::removed, "A set of Unicode strings giving Bash Tags suggested for removal.")
.def_readwrite("userlist_modified", &PluginTags::userlist_modified, "A boolean that is true if the suggestions contain metadata obtained from a loaded userlist.");
class_<PluginMetadata>(module, "PluginMetadata")
.def("get_simple_messages", &PluginMetadata::GetSimpleMessages);
class_<PluginMetadata>(module, "PluginMetadata", "Wraps :cpp:class:`loot::PluginMetadata`.")
.def("get_simple_messages",
&PluginMetadata::GetSimpleMessages,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Get the plugin's messages as SimpleMessage objects for the given language. Wraps :cpp:func:`GetSimpleMessages`.");
}
void bindVersionClasses(pybind11::module& module) {
class_<LootVersion>(module, "Version")
.def_readonly_static("major", &LootVersion::major)
.def_readonly_static("minor", &LootVersion::minor)
.def_readonly_static("patch", &LootVersion::patch)
.def_readonly_static("revision", &LootVersion::revision)
.def_static("string", LootVersion::GetVersionString);
// FIXME: For some reason the static properties have their docstrings ignored.
class_<LootVersion>(module, "Version", "Wraps :cpp:class:`loot::LootVersion`.")
.def_readonly_static("major", &LootVersion::major, "An unsigned integer giving the major version number. Read-only.")
.def_readonly_static("minor", &LootVersion::minor, "An unsigned integer giving the minor version number. Read-only.")
.def_readonly_static("patch", &LootVersion::patch, "An unsigned integer giving the patch version number. Read-only.")
.def_readonly_static("revision", &LootVersion::revision, "A Unicode string containing the Git commit hash that the wrapped libloot was built from.")
.def_static("string",
LootVersion::GetVersionString,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Returns the libloot version as a string of the form ``major.minor.patch``.");
class_<WrapperVersion>(module, "WrapperVersion")
.def_readonly_static("major", &WrapperVersion::major)
.def_readonly_static("minor", &WrapperVersion::minor)
.def_readonly_static("patch", &WrapperVersion::patch)
.def_readonly_static("revision", &WrapperVersion::revision)
.def_static("string", WrapperVersion::string);
class_<WrapperVersion>(module, "WrapperVersion", "Provides information about the version of libloot-python that is being run.")
.def_readonly_static("major", &WrapperVersion::major, "An unsigned integer giving the major version number. Read-only.")
.def_readonly_static("minor", &WrapperVersion::minor, "An unsigned integer giving the minor version number. Read-only.")
.def_readonly_static("patch", &WrapperVersion::patch, "An unsigned integer giving the patch version number. Read-only.")
.def_readonly_static("revision", &WrapperVersion::revision, "A Unicode string containing the Git commit hash that the Python module was built from.")
.def_static("string",
WrapperVersion::string,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Returns the module version as a string of the form ``major.minor.patch``.");
}
void bindInterfaceClasses(pybind11::module& module) {
class_<GameInterface, std::shared_ptr<GameInterface>>(module, "GameInterface")
.def("load_current_load_order_state", &GameInterface::LoadCurrentLoadOrderState)
.def("get_database", &GameInterface::GetDatabase);
class_<GameInterface, std::shared_ptr<GameInterface>>(module, "GameInterface", "Wraps :cpp:class:`loot::GameInterface`.")
.def("load_current_load_order_state",
&GameInterface::LoadCurrentLoadOrderState,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Load the current load order state, discarding any previously held state. Wraps :cpp:func:`LoadCurrentLoadOrderState`.")
.def("get_database",
&GameInterface::GetDatabase,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Get a database handle. Wraps :cpp:func:`GetDatabase`.")
.def("load_plugins",
&GameInterface::LoadPlugins,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Load the given plugins. Wraps :cpp:func:`LoadPlugins`.")
.def("get_plugin",
&GameInterface::GetPlugin,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Get the given loaded plugin. Wraps :cpp:func:`GetPlugin`.");
class_<DatabaseInterface, std::shared_ptr<DatabaseInterface>>(module, "DatabaseInterface")
.def("load_lists", &py::LoadLists, arg("masterlist_path"), arg("userlist_path") = "")
.def("update_masterlist", &py::UpdateMasterlist)
.def("get_masterlist_revision", &py::GetMasterlistRevision)
.def("get_plugin_metadata", &DatabaseInterface::GetPluginMetadata,
class_<DatabaseInterface, std::shared_ptr<DatabaseInterface>>(module, "DatabaseInterface", "Wraps :cpp:class:`loot::DatabaseInterface`.")
.def("load_lists",
&py::LoadLists,
arg("masterlist_path"),
arg("userlist_path") = "",
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Loads the masterlist and userlist from the paths specified. Wraps :cpp:func:`LoadLists`.")
.def("update_masterlist",
&py::UpdateMasterlist,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Updates the given masterlist using the given Git repository details. Wraps :cpp:func:`UpdateMasterlist`.")
.def("get_masterlist_revision",
&py::GetMasterlistRevision,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Gets the give masterlist's source control revision. Wraps :cpp:func:`GetMasterlistRevision`.")
.def("get_plugin_metadata",
&DatabaseInterface::GetPluginMetadata,
arg("plugin"),
arg("includeUserMetadata") = true,
arg("evaluateConditions") = false)
.def("get_plugin_tags", &GetPluginTags,
arg("evaluateConditions") = false,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Get all a plugin's loaded metadata. Wraps :cpp:func:`GetPluginMetadata`.")
.def("get_plugin_tags",
&GetPluginTags,
arg("plugin"),
arg("evaluateConditions") = false)
.def("get_plugin_cleanliness", &GetPluginCleanliness,
arg("evaluateConditions") = false,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Outputs the Bash Tags suggested for addition and removal by the database for the given plugin.")
.def("get_plugin_cleanliness",
&GetPluginCleanliness,
arg("plugin"),
arg("evaluateConditions") = false)
.def("write_minimal_list", &py::WriteMinimalList);
arg("evaluateConditions") = false,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Determines the database's knowledge of a plugin's cleanliness. Outputs whether the plugin should be cleaned or not, or if no data is available.")
.def("write_minimal_list",
&py::WriteMinimalList,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Writes a minimal metadata file containing only Bash Tag suggestions and/or cleanliness info from the loaded metadata. Wraps :cpp:func:`WriteMinimalList`.");
class_<PluginInterface, std::shared_ptr<PluginInterface>>(module, "PluginInterface")
.def_property_readonly("name",
&PluginInterface::GetName,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"The plugin's name. Read-only. Wraps :cpp:func:`GetName`.")
.def("is_master",
&PluginInterface::IsMaster,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Check if the plugin is a master. Wraps :cpp:func:`IsMaster`.")
.def("is_light_master",
&PluginInterface::IsLightMaster,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Check if the plugin is a light master. Wraps :cpp:func:`IsLightMaster`.")
.def("is_valid_as_light_master",
&PluginInterface::IsValidAsLightMaster,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Check if the plugin contains only records with FormIDs that are valid in a light master. Wraps :cpp:func:`IsValidAsLightMaster`.");
}
void bindClasses(pybind11::module& module) {
@@ -157,7 +233,10 @@ void bindClasses(pybind11::module& module) {
}
void bindFunctions(pybind11::module& module) {
module.def("set_logging_callback", &SetLoggingCallback);
module.def("set_logging_callback",
&py::SetLoggingCallback,
arg("callback"),
"Set the callback function that is called when logging. Wraps :cpp:func:`loot::SetLoggingCallback`.");
// Need to clear the stored logging callback when exiting, or Python will
// hang because the callback pointer is still stored by libloot.
@@ -166,12 +245,18 @@ void bindFunctions(pybind11::module& module) {
SetLoggingCallback(nullptr);
}));
module.def("is_compatible", &IsCompatible);
module.def("is_compatible",
&IsCompatible,
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Checks for API compatibility. Wraps :cpp:func:`loot::IsCompatible`.");
module.def("create_game_handle", &py::CreateGameHandle,
module.def("create_game_handle",
&py::CreateGameHandle,
arg("game"),
arg("game_path"),
arg("game_local_path") = "");
arg("game_local_path") = "",
pybind11::call_guard<pybind11::gil_scoped_release>(),
"Initialise a new game handle. Wraps :cpp:func:`loot::CreateGameHandle`.");
}
}
+62 -8
View File
@@ -20,29 +20,28 @@ from loot import is_compatible
from loot import set_logging_callback
def logging_callback(level, message):
pass
print(level, message)
set_logging_callback(logging_callback)
class GameFixture(unittest.TestCase):
game_path = os.path.join(u'.', u'Oblivion')
local_path = os.path.join(u'.', u'local')
master_filename = u'Oblivion.esm'
def setUp(self):
data_path = os.path.join(self.game_path, 'Data')
master_file = os.path.join(data_path, 'Oblivion.esm')
if not os.path.exists(data_path):
os.makedirs(data_path)
open(master_file, 'a').close()
open(self.master_file_path(), 'a').close()
if not os.path.exists(self.local_path):
os.makedirs(self.local_path)
def tearDown(self):
shutil.rmtree(self.game_path)
os.remove(self.master_file_path())
shutil.rmtree(self.local_path)
def master_file_path(self):
return os.path.join(self.game_path, 'Data', self.master_filename)
class TestLootApi(GameFixture):
def test_is_compatible(self):
self.assertFalse(is_compatible(0, 9, 0))
@@ -68,6 +67,61 @@ class TestLootApi(GameFixture):
db = game.get_database()
self.assertNotEqual(db, None)
class TestGameInterface(GameFixture):
def setUp(self):
super(TestGameInterface, self).setUp()
self.game = create_game_handle(GameType.tes4, self.game_path, self.local_path)
def test_load_plugins(self):
self.game.load_plugins([u'Blank.esm'], True)
self.game.load_plugins([u'Blank.esm'], False)
def test_get_plugin(self):
self.game.load_plugins([u'Blank.esm'], True)
plugin = self.game.get_plugin(u'Blank.esm')
self.assertNotEqual(plugin, None)
class TestPluginInterface(GameFixture):
game_path = os.path.join(u'.', u'SkyrimSE')
master_filename = u'Skyrim.esm'
def setUp(self):
super(TestPluginInterface, self).setUp()
self.game = create_game_handle(GameType.tes5se, self.game_path, self.local_path)
self.game.load_plugins([u'Blank.esm', u'Blank.esl'], False)
def test_name(self):
plugin = self.game.get_plugin(u'Blank.esm')
self.assertNotEqual(plugin, None)
self.assertEqual(plugin.name, u'Blank.esm')
def test_is_master(self):
plugin = self.game.get_plugin(u'Blank.esm')
self.assertNotEqual(plugin, None)
self.assertTrue(plugin.is_master())
def test_is_light_master(self):
plugin = self.game.get_plugin(u'Blank.esm')
self.assertNotEqual(plugin, None)
self.assertFalse(plugin.is_light_master())
plugin = self.game.get_plugin(u'Blank.esl')
self.assertNotEqual(plugin, None)
self.assertTrue(plugin.is_light_master())
def test_is_valid_as_light_master(self):
plugin = self.game.get_plugin(u'Blank.esm')
self.assertNotEqual(plugin, None)
self.assertTrue(plugin.is_valid_as_light_master())
class TestDatabaseInterface(GameFixture):
masterlist_path = os.path.join(os.path.dirname(__file__), u'masterlist.yaml')