13 Commits
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
Oliver Hamlet dcf9b8b96b Rename package and repository to libloot-python and module to loot 2019-11-06 20:17:40 +00:00
Oliver Hamlet 263322a75c Update libloot to v0.15.0 2019-11-06 18:58:02 +00:00
Oliver Hamlet b52cd9bd6b Update pybind11 to v2.4.2
v2.3.0 fixed GIL handling bugs, which means we need to explicitly stop
storing the Python logging callback before exiting, or the exit will
hang.
2019-10-22 17:43:30 +01:00
Oliver Hamlet 580bf87802 Fix artifact name matching on AppVeyor 2019-10-21 18:08:20 +01:00
Oliver Hamlet c144f7a66c Fix forwardslashes in package name on AppVeyor 2019-10-21 18:08:18 +01:00
Daniel Nunes ffd62f9e0c Include setup.py and __init__.py in archive for pip
So that the wrapper library can be installed with `pip install
<archive>`. The setup.py is Windows-specific, but CI only
produces Windows builds so that's not currently an issue.

The __init__.py re-exports the .pyd module so that the .pyd and
.dll can be installed into a subdirectory of site-packages for
neatness, and so that the installed module isn't just to work
around pip install being unable to install a pyd with no package.
2019-10-21 18:07:55 +01:00
Oliver Hamlet 937a36a5ae Set platform architecture using CMake's -A option
Introduced in CMake v3.1.0, I think it's a bit easier to remember than a
suffix on the generator name.
2019-06-22 13:46:43 +01:00
Oliver Hamlet 915c002ca6 Use PYBIND11_MODULE instead of PYBIND11_PLUGIN
The latter is deprecated.
2019-06-16 13:29:31 +01:00
Oliver Hamlet c19a9be579 Run tests using ctest on AppVeyor 2019-06-16 13:16:35 +01:00
Oliver Hamlet b393507657 Update libloot to v0.14.7 2019-06-16 13:03:53 +01:00
17 changed files with 446 additions and 368 deletions
+62 -28
View File
@@ -1,6 +1,6 @@
cmake_minimum_required (VERSION 2.8.12) cmake_minimum_required (VERSION 3.1)
project (loot_api_python) project (libloot-python)
include(ExternalProject) include(ExternalProject)
set(EXTERNAL_PROJECTS_PATH "${CMAKE_BINARY_DIR}/external/src") set(EXTERNAL_PROJECTS_PATH "${CMAKE_BINARY_DIR}/external/src")
@@ -14,6 +14,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Get Build Revision # Get Build Revision
############################## ##############################
set(LIBLOOT_PY_VERSION_MAJOR 4)
set(LIBLOOT_PY_VERSION_MINOR 0)
set(LIBLOOT_PY_VERSION_PATCH 2)
find_package(Git) find_package(Git)
IF (GIT_FOUND) IF (GIT_FOUND)
@@ -25,16 +29,18 @@ ELSE()
SET (GIT_COMMIT_STRING "unknown") SET (GIT_COMMIT_STRING "unknown")
ENDIF () ENDIF ()
message(STATUS "libloot-python version: ${LIBLOOT_PY_VERSION_MAJOR}.${LIBLOOT_PY_VERSION_MINOR}.${LIBLOOT_PY_VERSION_PATCH}")
message(STATUS "Git revision: ${GIT_COMMIT_STRING}") message(STATUS "Git revision: ${GIT_COMMIT_STRING}")
# Write to file. # Write to file.
configure_file("${CMAKE_SOURCE_DIR}/src/wrapper_version.cpp.in" "${CMAKE_BINARY_DIR}/generated/wrapper_version.cpp" @ONLY) configure_file("${CMAKE_SOURCE_DIR}/src/wrapper_version.cpp.in" "${CMAKE_BINARY_DIR}/generated/wrapper_version.cpp" @ONLY)
configure_file("${CMAKE_SOURCE_DIR}/python/setup.py" "${CMAKE_BINARY_DIR}/generated/setup.py" @ONLY)
####################################### #######################################
# pybind11 # pybind11
####################################### #######################################
set(PYBIND11_VERSION "2.2.4") set(PYBIND11_VERSION "2.4.2")
set(PYBIND11_URL "https://github.com/pybind/pybind11/archive/v${PYBIND11_VERSION}.tar.gz") 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_DOWNLOAD_PATH "${EXTERNAL_PROJECTS_PATH}/pybind11-${PYBIND11_VERSION}.tar.gz")
set(PYBIND11_EXTRACTED_PATH "${EXTERNAL_PROJECTS_PATH}/pybind11-${PYBIND11_VERSION}") set(PYBIND11_EXTRACTED_PATH "${EXTERNAL_PROJECTS_PATH}/pybind11-${PYBIND11_VERSION}")
@@ -50,17 +56,17 @@ execute_process(COMMAND ${CMAKE_COMMAND} -E tar xfz ${PYBIND11_DOWNLOAD_PATH}
add_subdirectory(${PYBIND11_EXTRACTED_PATH}) add_subdirectory(${PYBIND11_EXTRACTED_PATH})
####################################### #######################################
# LOOT API # libloot
####################################### #######################################
if (CMAKE_SYSTEM_NAME MATCHES "Windows") if (CMAKE_SYSTEM_NAME MATCHES "Windows")
if (NOT "${CMAKE_GENERATOR}" MATCHES "(Win64|IA64)") if (NOT "${CMAKE_GENERATOR_PLATFORM}" MATCHES "x64")
set(LIBLOOT_URL "https://github.com/loot/libloot/releases/download/0.14.5/libloot-0.14.5-0-g68ecc02_dev-win32.7z") set(LIBLOOT_URL "https://github.com/loot/libloot/releases/download/0.15.0/libloot-0.15.0-0-g629fdab_master-win32.7z")
else() else()
set(LIBLOOT_URL "https://github.com/loot/libloot/releases/download/0.14.5/libloot-0.14.5-0-g68ecc02_dev-win64.7z") set(LIBLOOT_URL "https://github.com/loot/libloot/releases/download/0.15.0/libloot-0.15.0-0-g629fdab_master-win64.7z")
endif() endif()
else() else()
set(LIBLOOT_URL "https://github.com/loot/libloot/releases/download/0.14.5/libloot.tar.xz") set(LIBLOOT_URL "https://github.com/loot/libloot/releases/download/0.15.0/libloot.tar.xz")
endif() endif()
ExternalProject_Add(libloot ExternalProject_Add(libloot
@@ -78,20 +84,38 @@ link_directories(${LIBLOOT_EXTRACTED_PATH})
set(LIBLOOT_STATIC_LIBRARY "${CMAKE_STATIC_LIBRARY_PREFIX}loot${CMAKE_STATIC_LIBRARY_SUFFIX}") 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}") 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 # Python Module
####################################### #######################################
pybind11_add_module(loot_api "${CMAKE_SOURCE_DIR}/src/main.cpp" pybind11_add_module(libloot-python "${CMAKE_SOURCE_DIR}/src/main.cpp"
"${CMAKE_SOURCE_DIR}/src/convenience.cpp" "${CMAKE_SOURCE_DIR}/src/convenience.cpp"
"${CMAKE_BINARY_DIR}/generated/wrapper_version.cpp") "${CMAKE_BINARY_DIR}/generated/wrapper_version.cpp")
add_dependencies(loot_api libloot) # Set the target output name to loot to follow common Python library naming
# conventions (no lib, no -python).
set_target_properties(libloot-python PROPERTIES OUTPUT_NAME loot)
add_dependencies(libloot-python libloot testing-plugins)
if (CMAKE_SYSTEM_NAME MATCHES "Windows") if (CMAKE_SYSTEM_NAME MATCHES "Windows")
target_link_libraries(loot_api PRIVATE ${LIBLOOT_STATIC_LIBRARY}) target_link_libraries(libloot-python PRIVATE ${LIBLOOT_STATIC_LIBRARY})
else() else()
target_link_libraries(loot_api PRIVATE ${LIBLOOT_SHARED_LIBRARY}) target_link_libraries(libloot-python PRIVATE ${LIBLOOT_SHARED_LIBRARY})
endif() endif()
######################################## ########################################
@@ -100,37 +124,50 @@ endif()
find_package(PythonInterp REQUIRED) find_package(PythonInterp REQUIRED)
# Copy the API binary to the build directory. # Copy the libloot binary to the build directory.
add_custom_command(TARGET loot_api POST_BUILD add_custom_command(TARGET libloot-python POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${LIBLOOT_EXTRACTED_PATH}/${LIBLOOT_SHARED_LIBRARY}" "${LIBLOOT_EXTRACTED_PATH}/${LIBLOOT_SHARED_LIBRARY}"
"$<TARGET_FILE_DIR:loot_api>/${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() enable_testing()
add_test(NAME python add_test(NAME python
WORKING_DIRECTORY $<TARGET_FILE_DIR:loot_api> WORKING_DIRECTORY $<TARGET_FILE_DIR:libloot-python>
COMMAND "${PYTHON_EXECUTABLE}" ${CMAKE_SOURCE_DIR}/test/test.py) COMMAND "${PYTHON_EXECUTABLE}" ${CMAKE_SOURCE_DIR}/test/test.py)
######################################## ########################################
# Install # Install
######################################## ########################################
install(TARGETS loot_api install(TARGETS libloot-python
DESTINATION "." DESTINATION "loot"
CONFIGURATIONS Release RelWithDebInfo) CONFIGURATIONS Release RelWithDebInfo)
install(FILES "${LIBLOOT_EXTRACTED_PATH}/${LIBLOOT_SHARED_LIBRARY}" install(FILES "${LIBLOOT_EXTRACTED_PATH}/${LIBLOOT_SHARED_LIBRARY}"
"${CMAKE_SOURCE_DIR}/docs/README.md" "${CMAKE_SOURCE_DIR}/python/loot/__init__.py"
DESTINATION "loot"
CONFIGURATIONS Release RelWithDebInfo)
install(FILES "${CMAKE_SOURCE_DIR}/docs/README.md"
"${CMAKE_BINARY_DIR}/generated/setup.py"
"${CMAKE_SOURCE_DIR}/LICENSE"
DESTINATION "." DESTINATION "."
CONFIGURATIONS Release RelWithDebInfo) CONFIGURATIONS Release RelWithDebInfo)
IF (MSVC) IF (MSVC)
install(FILES $<TARGET_PDB_FILE:loot_api> install(FILES $<TARGET_PDB_FILE:libloot-python>
DESTINATION . DESTINATION "loot"
OPTIONAL OPTIONAL
CONFIGURATIONS RelWithDebInfo CONFIGURATIONS RelWithDebInfo
RENAME loot_api_python.pdb) RENAME libloot-python.pdb)
ENDIF () ENDIF ()
######################################## ########################################
@@ -151,16 +188,13 @@ IF (GIT_FOUND)
ELSEIF (DEFINED ENV{TRAVIS_BRANCH}) ELSEIF (DEFINED ENV{TRAVIS_BRANCH})
set(GIT_DESCRIBE_STRING "${GIT_DESCRIBE_STRING}_$ENV{TRAVIS_BRANCH}") set(GIT_DESCRIBE_STRING "${GIT_DESCRIBE_STRING}_$ENV{TRAVIS_BRANCH}")
ENDIF() ENDIF()
string(REPLACE "/" "-" GIT_DESCRIBE_STRING ${GIT_DESCRIBE_STRING})
ELSE() ELSE()
SET (GIT_DESCRIBE_STRING "unknown-version") SET (GIT_DESCRIBE_STRING "unknown-version")
ENDIF () ENDIF ()
if (CMAKE_SYSTEM_NAME MATCHES "Windows") set(CPACK_GENERATOR "ZIP")
set(CPACK_GENERATOR "7Z")
else()
set(CPACK_GENERATOR "TXZ")
endif()
set(CPACK_PACKAGE_VERSION "${GIT_DESCRIBE_STRING}-python$ENV{PYTHON_VERSION}") set(CPACK_PACKAGE_VERSION "${GIT_DESCRIBE_STRING}-python$ENV{PYTHON_VERSION}")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/package") set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/package")
+6 -6
View File
@@ -1,20 +1,20 @@
LOOT API Python Module libloot-python
======================= ==============
[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/k2ugge3po7254o1o/branch/master?svg=true)](https://ci.appveyor.com/project/LOOT/loot-api-python/branch/master) [![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/) [![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). 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 ## Downloads
Snapshot builds are available on [Bintray](https://bintray.com/loot/snapshots/loot-api-python). 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:
``` ```
loot_api_python-<short revision ID>-python<python version>-win<architecture>.7z libloot-python-<short revision ID>-python<python version>-win<architecture>.zip
``` ```
For example `loot_api_python-94de368-python2.7-win32.7z` 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 ## Documentation
+17 -15
View File
@@ -20,6 +20,9 @@ environment:
install: 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://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: before_build:
- ps: $env:PYTHON_DIRECTORY_VERSION=$env:PYTHON_VERSION.Replace(".", "") - ps: $env:PYTHON_DIRECTORY_VERSION=$env:PYTHON_VERSION.Replace(".", "")
@@ -30,28 +33,27 @@ before_build:
if ($env:PLATFORM -eq 'Win32') { if ($env:PLATFORM -eq 'Win32') {
cmake .. -G "Visual Studio 15 2017" -DPYTHON_EXECUTABLE="C:\Python${env:PYTHON_DIRECTORY_VERSION}\python.exe" cmake .. -G "Visual Studio 15 2017" -DPYTHON_EXECUTABLE="C:\Python${env:PYTHON_DIRECTORY_VERSION}\python.exe"
} else { } else {
cmake .. -G "Visual Studio 15 2017 Win64" -DPYTHON_EXECUTABLE="C:\Python${env:PYTHON_DIRECTORY_VERSION}-x64\python.exe" cmake .. -G "Visual Studio 15 2017" -A x64 -DPYTHON_EXECUTABLE="C:\Python${env:PYTHON_DIRECTORY_VERSION}-x64\python.exe"
} }
build: build:
verbosity: minimal verbosity: minimal
project: 'c:\projects\loot-api-python\build\loot_api_python.sln' project: 'c:\projects\libloot-python\build\libloot-python.sln'
test_script: test_script:
- cd %APPVEYOR_BUILD_FOLDER%\build\%CONFIGURATION% - cd %APPVEYOR_BUILD_FOLDER%\build
- ps: $env:ARCHITECTURE=$env:PLATFORM.Substring($env:PLATFORM.Length - 2) - ctest -C %CONFIGURATION%
- py -%PYTHON_VERSION%-%ARCHITECTURE% -m unittest test
after_test: after_test:
- cd %APPVEYOR_BUILD_FOLDER%\build - cd %APPVEYOR_BUILD_FOLDER%\build
- cpack -C %CONFIGURATION% - cpack -C %CONFIGURATION%
# AppVeyor checks out a specific commit, but that means the archive script
# can't tell which branch it's on, so rename the 7z archives.
- ps: $env:GIT_DESCRIBE = ((git describe --tags --long --always) | Out-String) -replace "`n|`r", "" - 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: artifacts:
- path: build\package\loot_api_python-$(GIT_DESCRIBE)_$(APPVEYOR_REPO_BRANCH)-python$(PYTHON_VERSION)-win$(ARCHITECTURE).7z - path: build\package\libloot-python-$(PACKAGE_VERSION)-python$(PYTHON_VERSION)-win$(ARCHITECTURE).zip
name: loot_api_python name: libloot-python
deploy: deploy:
- provider: BinTray - provider: BinTray
@@ -60,22 +62,22 @@ deploy:
secure: PgsEA6TjHVf718zMnK7J/fT1hUAVNKBeWhpYgYaeCyeZk37VT4Ics6j7+B7ElLEr secure: PgsEA6TjHVf718zMnK7J/fT1hUAVNKBeWhpYgYaeCyeZk37VT4Ics6j7+B7ElLEr
subject: loot subject: loot
repo: snapshots repo: snapshots
package: loot-api-python package: libloot-python
version: $(GIT_DESCRIBE)_$(APPVEYOR_REPO_BRANCH) version: $(PACKAGE_VERSION)
publish: true publish: true
artifact: loot_api_python artifact: libloot-python
- provider: GitHub - provider: GitHub
tag: $(APPVEYOR_REPO_TAG_NAME) tag: $(APPVEYOR_REPO_TAG_NAME)
release: LOOT API Python Module v$(APPVEYOR_REPO_TAG_NAME) release: libloot-python v$(APPVEYOR_REPO_TAG_NAME)
description: | 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. 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: auth_token:
secure: yDqT5l/e5MntbW99V6+MHlfFgNv+UIogFfeyUVqtFk5lFRB/dAraLLwKCLl6y+DH secure: yDqT5l/e5MntbW99V6+MHlfFgNv+UIogFfeyUVqtFk5lFRB/dAraLLwKCLl6y+DH
artifact: loot_api_python artifact: libloot-python
draft: false draft: false
on: on:
appveyor_repo_tag: true appveyor_repo_tag: true
on_success: on_success:
- ps: python "$env:APPVEYOR_BUILD_FOLDER\delete_old_bintray_versions.py" -g loot/loot-api-python -b loot/snapshots/loot-api-python -u wrinklyninja -k $env:bintray_auth_token -t $env:github_auth_token -n 30 - 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
+4 -4
View File
@@ -1,13 +1,13 @@
LOOT API Python Wrapper libloot-python
======================= ==============
This archive contains a build of the LOOT API Python wrapper module and its corresponding 32-bit Windows build of the LOOT API. 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 2017 x86 redistributable](https://download.visualstudio.microsoft.com/download/pr/749aa419-f9e4-4578-a417-a43786af205e/d59197078cc425377be301faba7dd87a/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. to be installed.
See the [online documentation](http://loot-api-python.readthedocs.org/) for more information. See the [online documentation](http://loot-api-python.readthedocs.org/) for more information.
The library source code is available [on GitHub](https://github.com/loot/loot-api-python). The library source code is available [on GitHub](https://github.com/loot/libloot-python).
--- ---
+11 -9
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# LOOT API Python Module documentation build configuration file, created by # libloot-python documentation build configuration file, created by
# sphinx-quickstart on Mon Aug 15 07:11:32 2016. # sphinx-quickstart on Mon Aug 15 07:11:32 2016.
# #
# This file is execfile()d with the current directory set to its # This file is execfile()d with the current directory set to its
@@ -22,7 +22,7 @@
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('../build/Release')) sys.path.insert(0, os.path.abspath('../build/RelWithDebInfo'))
# -- General configuration ------------------------------------------------ # -- 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 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
] ]
@@ -54,7 +55,7 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'LOOT API Python Module' project = u'libloot-python'
copyright = u'2019, WrinklyNinja' copyright = u'2019, WrinklyNinja'
author = u'WrinklyNinja' author = u'WrinklyNinja'
@@ -139,7 +140,7 @@ html_theme = 'sphinx_rtd_theme'
# The name for this set of Sphinx documents. # The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default. # "<project> v<release> documentation" by default.
# #
# html_title = u'LOOT API Python Module v;atest' # html_title = u'libloot-python v;atest'
# A shorter title for the navigation bar. Default is the same as html_title. # A shorter title for the navigation bar. Default is the same as html_title.
# #
@@ -265,7 +266,7 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'LOOTAPIPythonModule.tex', u'LOOT API Python Module Documentation', (master_doc, 'libloot-python.tex', u'libloot-python Documentation',
u'WrinklyNinja', 'manual'), u'WrinklyNinja', 'manual'),
] ]
@@ -307,7 +308,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
(master_doc, 'lootapipythonmodule', u'LOOT API Python Module Documentation', (master_doc, 'libloot-python', u'libloot-python Documentation',
[author], 1) [author], 1)
] ]
@@ -322,8 +323,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'LOOTAPIPythonModule', u'LOOT API Python Module Documentation', (master_doc, 'libloot-python', u'libloot-python Documentation',
author, 'LOOTAPIPythonModule', 'One line description of project.', author, 'libloot-python', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]
@@ -346,5 +347,6 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = { intersphinx_mapping = {
'loot_api': ('http://loot.readthedocs.io/en/0.14.5/', None), 'loot': ('https://loot.readthedocs.io/en/0.15.0/', None),
'python': ('https://docs.python.org/3', None)
} }
+3 -3
View File
@@ -1,10 +1,10 @@
.. LOOT API Python Module documentation master file, created by .. libloot-python documentation master file, created by
sphinx-quickstart on Mon Aug 15 07:11:32 2016. sphinx-quickstart on Mon Aug 15 07:11:32 2016.
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
LOOT API Python Module libloot-python
====================== ==============
Contents: Contents:
+12 -203
View File
@@ -2,199 +2,18 @@
API Reference API Reference
************* *************
As this API is just a wrapper for LOOT's C++ API, its documentation is linked to As this API is just a wrapper for libloot's C++ API, its documentation is linked
for all non-Python-specific information. to for all non-Python-specific information.
Enumerations The wrapped enumeration types are classes in Python, but the distinction
============
The wrapped enumeration types below are classes in Python, but the distinction
makes no difference in practice, so they're grouped here for semantics. All 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_api.GameType .. automodule:: loot
:members:
:exclude-members: Version, WrapperVersion
Wraps :cpp:type:`loot::GameType` to expose the LOOT API's game codes. .. py:class:: loot.Version
.. py:attribute:: fo3
.. py:attribute:: fo4
.. py:attribute:: fonv
.. py:attribute:: tes4
.. py:attribute:: tes5
.. py:attribute:: tes5se
.. py:class:: loot_api.LogLevel
Wraps :cpp:type:`loot::LogLevel` to expose the LOOT API'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_api.MessageType
Wraps :cpp:type:`loot::MessageType` to expose the LOOT API's message type
codes.
.. py:attribute:: error
.. py:attribute:: say
.. py:attribute:: warn
.. py:class:: loot_api.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_api.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_api.SimpleMessage
Wraps :cpp:class:`loot::SimpleMessage`.
.. py:attribute:: type
A :py:class:`loot_api.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_api.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_api.set_logging_callback(callback) -> NoneType
Set the callback function that is called when logging. Wraps
:cpp:func:`loot::SetLoggingCallback`.
.. py:function:: loot_api.is_compatible(int, int, int) -> bool
Checks for API compatibility. Wraps :cpp:func:`loot::IsCompatible`.
.. py:function:: loot_api.InitialiseLocale(unicode = u'') -> NoneType
Initialise the current global locale using the given ID. A blank value means
that the system default locale will be initialised. Wraps
:cpp:func:`loot::InitialiseLocale`.
.. py:function:: loot_api.create_game_handle(game : loot_api.GameType, game_path : unicode, [game_local_path : unicode = u'']) -> loot_api.GameInterface
Initialise a new game handle. Wraps :cpp:func:`loot::CreateGameHandle`.
Classes
=======
.. py:class:: loot_api.GameInterface
Wraps :cpp:class:`loot::GameInterface`.
.. py:function:: loot_api.get_database() -> loot_api.DatabaseInterface
Get a database handle. Wraps :cpp:func:`loot::GetDatabase`.
.. py:function:: loot_api.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_api.DatabaseInterface
Wraps :cpp:class:`loot::DatabaseInterface`.
.. py:method:: get_masterlist_revision(loot_api.DatabaseInterface, unicode, bool) -> loot_api.MasterlistInfo
Gets the give masterlist’s source control revision. Wraps :cpp:func:`GetMasterlistRevision`.
.. py:method:: get_plugin_metadata(loot_api.DatabaseInterface, plugin : unicode, [includeUserMetadata : bool = True, [evaluateConditions : bool = False]]) -> loot_api.PluginMetadata
Get all a plugin’s loaded metadata. Wraps :cpp:func:`GetPluginMetadata`.
.. py:method:: get_plugin_cleanliness(loot_api.DatabaseInterface, plugin : unicode, [evaluateConditions : bool = False]) -> loot_api.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_api.DatabaseInterface, plugin : unicode, [evaluateConditions : bool = False]) -> loot_api.PluginTags
Outputs the Bash Tags suggested for addition and removal by the database for the given plugin.
.. py:method:: load_lists(loot_api.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_api.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_api.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`.
.. py:class:: loot_api.Version
Wraps :cpp:class:`loot::LootVersion`. Wraps :cpp:class:`loot::LootVersion`.
@@ -214,14 +33,13 @@ Classes
A Unicode string containing the SHA-1 of the Git revision that the wrapped C++ API was built from. 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`` Returns the API version as a string of the form ``major.minor.patch``
.. py:class:: loot_api.WrapperVersion .. py:class:: loot.WrapperVersion
Provides information about the version of the LOOT API Python wrapper that is Provides information about the version of libloot-python that is being run.
being run.
.. py:attribute:: major .. py:attribute:: major
@@ -239,15 +57,6 @@ Classes
A Unicode string containing the SHA-1 of the Git revision that the wrapped C++ API was built from. 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`` Returns the API version as a string of the form ``major.minor.patch``
.. py:class:: loot_api.PluginMetadata
Wraps :cpp:class:`loot::PluginMetadata`.
.. py:method:: get_simple_messages(loot_api.PluginMetadata, unicode) -> list<loot_api.SimpleMessage>
Get the plugin’s messages as SimpleMessage objects for the given language.
Wraps :cpp:func:`GetPluginMessages`.
+7 -8
View File
@@ -7,8 +7,8 @@ Installing the wrapper
Build archives contain two binaries: Build archives contain two binaries:
* ``loot_api.pyd`` is the Python wrapper * ``loot.*.pyd`` is the Python wrapper
* ``loot_api.dll`` is the C++ API DLL that the Python wrapper was built against. * ``loot.dll`` is the C++ library DLL that the Python wrapper was built against.
The C++ DLL requires the `Visual C++ 2017 Redistributable (x86)`_ The C++ DLL requires the `Visual C++ 2017 Redistributable (x86)`_
to be installed. to be installed.
@@ -27,10 +27,10 @@ Checking Compatibility
To check if the module loaded is compatible with the version of the API that you To check if the module loaded is compatible with the version of the API that you
developed against:: developed against::
>>> import loot_api >>> import loot
>>> loot_api.is_compatible(0,14,0) >>> loot.is_compatible(0,14,0)
True True
>>> loot_api.is_compatible(0,9,0) >>> loot.is_compatible(0,9,0)
False False
Getting a Plugin's Bash Tag Suggestions Getting a Plugin's Bash Tag Suggestions
@@ -38,9 +38,8 @@ Getting a Plugin's Bash Tag Suggestions
To get a plugin's Bash Tag suggestions from a ``masterlist.yaml`` metadata file:: To get a plugin's Bash Tag suggestions from a ``masterlist.yaml`` metadata file::
>>> import loot_api >>> import loot
>>> loot_api.initialise_locale() >>> db = loot.create_game_handle(loot.GameType.tes4, 'C:\\path\\to\\oblivion\\directory')
>>> db = loot_api.create_game_handle(loot_api.GameType.tes4, 'C:\\path\\to\\oblivion\\directory')
>>> db.load_lists('masterlist.yaml') >>> db.load_lists('masterlist.yaml')
>>> tags = db.get_plugin_tags(u'Unofficial Oblivion Patch.esp') >>> tags = db.get_plugin_tags(u'Unofficial Oblivion Patch.esp')
>>> tags.added >>> tags.added
+1
View File
@@ -0,0 +1 @@
from .loot import *
+89
View File
@@ -0,0 +1,89 @@
import itertools
import os
import platform
import subprocess
import sys
import tempfile
import threading
import time
from contextlib import contextmanager
from setuptools import Distribution, find_packages, setup
try: # python 3
import winreg
from urllib.request import urlretrieve
except ImportError: # python 2
import _winreg as winreg
from urllib import urlretrieve
MSVC_MIN_VERSION = (14, 0, 24215)
class BinaryDistribution(Distribution):
def has_ext_modules(foo):
return True
def is_msvc_redist_installed(major, minor, build):
if platform.machine().endswith("64"): # check if os is 64bit
sub_key = "SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\x64"
else:
sub_key = "SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\x86"
try:
key_handle = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, sub_key)
runtime_installed = winreg.QueryValueEx(key_handle, "Installed")[0]
installed_major = winreg.QueryValueEx(key_handle, "Major")[0]
installed_minor = winreg.QueryValueEx(key_handle, "Minor")[0]
installed_build = winreg.QueryValueEx(key_handle, "Bld")[0]
return (
runtime_installed != 0
and installed_major >= major
and installed_minor >= minor
and installed_build >= build
)
except WindowsError as exc:
return False
with open("README.md", "r") as fh:
long_description = fh.read()
def install_msvc_redist():
url = (
"https://download.microsoft.com/download/6/A/A/"
"6AA4EDFF-645B-48C5-81CC-ED5963AEAD48/vc_redist.x86.exe"
)
dl_dir = tempfile.mkdtemp()
dl_file = os.path.join(dl_dir, "vc_redist.exe")
urlretrieve(url, dl_file)
subprocess.call([dl_file, "/quiet"])
# trying to remove the temp folder triggers
# an error during py2 pip install
if not is_msvc_redist_installed(*MSVC_MIN_VERSION):
install_msvc_redist()
# The version string is substituted in by CMake.
setup(
name="libloot-python",
version="@LIBLOOT_PY_VERSION_MAJOR@.@LIBLOOT_PY_VERSION_MINOR@.@LIBLOOT_PY_VERSION_PATCH@",
description="A Python module that wraps libloot.",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/loot/libloot-python",
packages=find_packages(),
include_package_data=True,
package_data={"loot": ["loot*.pyd", "loot.dll"]},
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='>=2.7',
)
+2 -2
View File
@@ -22,8 +22,8 @@ along with LOOT. If not, see
<https://www.gnu.org/licenses/>. <https://www.gnu.org/licenses/>.
*/ */
#ifndef LOOT_API_PYTHON_CONVENIENCE #ifndef LIBLOOT_PYTHON_CONVENIENCE
#define LOOT_API_PYTHON_CONVENIENCE #define LIBLOOT_PYTHON_CONVENIENCE
#include <memory> #include <memory>
+145 -57
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) { MasterlistInfo GetMasterlistRevision(std::shared_ptr<DatabaseInterface> db, std::string masterlistPath, bool getShortId) {
return db->GetMasterlistRevision(u8path(masterlistPath), 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) { 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("tes4", GameType::tes4)
.value("tes5", GameType::tes5) .value("tes5", GameType::tes5)
.value("tes5se", GameType::tes5se) .value("tes5se", GameType::tes5se)
@@ -71,7 +82,7 @@ void bindEnums(pybind11::module& module) {
.value("fo4", GameType::fo4) .value("fo4", GameType::fo4)
.value("fo4vr", GameType::fo4vr); .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("trace", LogLevel::trace)
.value("debug", LogLevel::debug) .value("debug", LogLevel::debug)
.value("info", LogLevel::info) .value("info", LogLevel::info)
@@ -79,75 +90,140 @@ void bindEnums(pybind11::module& module) {
.value("error", LogLevel::error) .value("error", LogLevel::error)
.value("fatal", LogLevel::fatal); .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("say", MessageType::say)
.value("warn", MessageType::warn) .value("warn", MessageType::warn)
.value("error", MessageType::error); .value("error", MessageType::error);
enum_<PluginCleanliness>(module, "PluginCleanliness") 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) .value("clean", PluginCleanliness::clean, "Indicates that the plugin is clean.")
.value("dirty", PluginCleanliness::dirty) .value("dirty", PluginCleanliness::dirty, "Indicates that the plugin is dirty.")
.value("do_not_clean", PluginCleanliness::do_not_clean) .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); .value("unknown", PluginCleanliness::unknown, "Indicates that no data is available on whether the plugin is dirty or not.");
} }
void bindMetadataClasses(pybind11::module& module) { void bindMetadataClasses(pybind11::module& module) {
class_<MasterlistInfo>(module, "MasterlistInfo") class_<MasterlistInfo>(module, "MasterlistInfo", "Wraps :cpp:class:`loot::MasterlistInfo`.")
.def_readwrite("revision_id", &MasterlistInfo::revision_id) .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) .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); .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") class_<SimpleMessage>(module, "SimpleMessage", "Wraps :cpp:class:`loot::SimpleMessage`.")
.def_readwrite("type", &SimpleMessage::type) .def_readwrite("type", &SimpleMessage::type, "A :py:class:`loot.MessageType` giving the message type.")
.def_readwrite("language", &SimpleMessage::language) .def_readwrite("language", &SimpleMessage::language, "A Unicode string giving the message text language.")
.def_readwrite("text", &SimpleMessage::text) .def_readwrite("text", &SimpleMessage::text, "A Unicode string containing the message text.")
.def_readwrite("condition", &SimpleMessage::condition); .def_readwrite("condition", &SimpleMessage::condition, "A Unicode string containing the message condition.");
class_<PluginTags>(module, "PluginTags") class_<PluginTags>(module, "PluginTags")
.def_readwrite("added", &PluginTags::added) .def_readwrite("added", &PluginTags::added, "A set of Unicode strings giving Bash Tags suggested for addition.")
.def_readwrite("removed", &PluginTags::removed) .def_readwrite("removed", &PluginTags::removed, "A set of Unicode strings giving Bash Tags suggested for removal.")
.def_readwrite("userlist_modified", &PluginTags::userlist_modified); .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") class_<PluginMetadata>(module, "PluginMetadata", "Wraps :cpp:class:`loot::PluginMetadata`.")
.def("get_simple_messages", &PluginMetadata::GetSimpleMessages); .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) { void bindVersionClasses(pybind11::module& module) {
class_<LootVersion>(module, "Version") // FIXME: For some reason the static properties have their docstrings ignored.
.def_readonly_static("major", &LootVersion::major) class_<LootVersion>(module, "Version", "Wraps :cpp:class:`loot::LootVersion`.")
.def_readonly_static("minor", &LootVersion::minor) .def_readonly_static("major", &LootVersion::major, "An unsigned integer giving the major version number. Read-only.")
.def_readonly_static("patch", &LootVersion::patch) .def_readonly_static("minor", &LootVersion::minor, "An unsigned integer giving the minor version number. Read-only.")
.def_readonly_static("revision", &LootVersion::revision) .def_readonly_static("patch", &LootVersion::patch, "An unsigned integer giving the patch version number. Read-only.")
.def_static("string", LootVersion::GetVersionString); .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") class_<WrapperVersion>(module, "WrapperVersion", "Provides information about the version of libloot-python that is being run.")
.def_readonly_static("major", &WrapperVersion::major) .def_readonly_static("major", &WrapperVersion::major, "An unsigned integer giving the major version number. Read-only.")
.def_readonly_static("minor", &WrapperVersion::minor) .def_readonly_static("minor", &WrapperVersion::minor, "An unsigned integer giving the minor version number. Read-only.")
.def_readonly_static("patch", &WrapperVersion::patch) .def_readonly_static("patch", &WrapperVersion::patch, "An unsigned integer giving the patch version number. Read-only.")
.def_readonly_static("revision", &WrapperVersion::revision) .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); .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) { void bindInterfaceClasses(pybind11::module& module) {
class_<GameInterface, std::shared_ptr<GameInterface>>(module, "GameInterface") class_<GameInterface, std::shared_ptr<GameInterface>>(module, "GameInterface", "Wraps :cpp:class:`loot::GameInterface`.")
.def("load_current_load_order_state", &GameInterface::LoadCurrentLoadOrderState) .def("load_current_load_order_state",
.def("get_database", &GameInterface::GetDatabase); &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") class_<DatabaseInterface, std::shared_ptr<DatabaseInterface>>(module, "DatabaseInterface", "Wraps :cpp:class:`loot::DatabaseInterface`.")
.def("load_lists", &py::LoadLists, arg("masterlist_path"), arg("userlist_path") = "") .def("load_lists",
.def("update_masterlist", &py::UpdateMasterlist) &py::LoadLists,
.def("get_masterlist_revision", &py::GetMasterlistRevision) arg("masterlist_path"),
.def("get_plugin_metadata", &DatabaseInterface::GetPluginMetadata, 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("plugin"),
arg("includeUserMetadata") = true, arg("includeUserMetadata") = true,
arg("evaluateConditions") = false) arg("evaluateConditions") = false,
.def("get_plugin_tags", &GetPluginTags, 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("plugin"),
arg("evaluateConditions") = false) arg("evaluateConditions") = false,
.def("get_plugin_cleanliness", &GetPluginCleanliness, 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("plugin"),
arg("evaluateConditions") = false) arg("evaluateConditions") = false,
.def("write_minimal_list", &py::WriteMinimalList); 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) { void bindClasses(pybind11::module& module) {
@@ -157,25 +233,37 @@ void bindClasses(pybind11::module& module) {
} }
void bindFunctions(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`.");
module.def("is_compatible", &IsCompatible); // Need to clear the stored logging callback when exiting, or Python will
// hang because the callback pointer is still stored by libloot.
auto atexit = pybind11::module::import("atexit");
atexit.attr("register")(pybind11::cpp_function([]() {
SetLoggingCallback(nullptr);
}));
module.def("initialise_locale", &InitialiseLocale, arg("id") = ""); 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"),
arg("game_path"), 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`.");
} }
} }
PYBIND11_PLUGIN(loot_api) { PYBIND11_MODULE(loot, module) {
pybind11::module module("loot_api", "A Python module that wraps the LOOT API, generated by pybind11."); module.doc() = "A Python module that wraps libloot, generated by pybind11.";
loot::bindEnums(module); loot::bindEnums(module);
loot::bindClasses(module); loot::bindClasses(module);
loot::bindFunctions(module); loot::bindFunctions(module);
return module.ptr();
} }
+3 -3
View File
@@ -21,11 +21,11 @@
along with LOOT. If not, see along with LOOT. If not, see
<https://www.gnu.org/licenses/>. <https://www.gnu.org/licenses/>.
*/ */
#ifndef LOOT_PLUGIN_CLEANLINESS #ifndef LIBLOOT_PYTHON_PLUGIN_CLEANLINESS
#define LOOT_PLUGIN_CLEANLINESS #define LIBLOOT_PYTHON_PLUGIN_CLEANLINESS
/** /**
* The namespace used by the LOOT API. * The namespace used by libloot.
*/ */
namespace loot { namespace loot {
/** /**
+2 -2
View File
@@ -21,8 +21,8 @@
along with LOOT. If not, see along with LOOT. If not, see
<https://www.gnu.org/licenses/>. <https://www.gnu.org/licenses/>.
*/ */
#ifndef LOOT_PLUGIN_TAGS #ifndef LIBLOOT_PYTHON_PLUGIN_TAGS
#define LOOT_PLUGIN_TAGS #define LIBLOOT_PYTHON_PLUGIN_TAGS
#include <set> #include <set>
#include <string> #include <string>
+3 -3
View File
@@ -25,9 +25,9 @@
#include "wrapper_version.h" #include "wrapper_version.h"
namespace loot { namespace loot {
const unsigned int WrapperVersion::major = 4; const unsigned int WrapperVersion::major = @LIBLOOT_PY_VERSION_MAJOR@;
const unsigned int WrapperVersion::minor = 0; const unsigned int WrapperVersion::minor = @LIBLOOT_PY_VERSION_MINOR@;
const unsigned int WrapperVersion::patch = 2; const unsigned int WrapperVersion::patch = @LIBLOOT_PY_VERSION_PATCH@;
const std::string WrapperVersion::revision = "@GIT_COMMIT_STRING@"; const std::string WrapperVersion::revision = "@GIT_COMMIT_STRING@";
std::string WrapperVersion::string() { std::string WrapperVersion::string() {
+2 -2
View File
@@ -22,8 +22,8 @@ along with LOOT. If not, see
<https://www.gnu.org/licenses/>. <https://www.gnu.org/licenses/>.
*/ */
#ifndef LOOT_API_PYTHON_WRAPPER_VERSION #ifndef LIBLOOT_PYTHON_WRAPPER_VERSION
#define LOOT_API_PYTHON_WRAPPER_VERSION #define LIBLOOT_PYTHON_WRAPPER_VERSION
#include <string> #include <string>
+77 -23
View File
@@ -9,51 +9,50 @@ import unittest
sys.path.append(os.getcwd()) sys.path.append(os.getcwd())
from loot_api import Version from loot import Version
from loot_api import WrapperVersion from loot import WrapperVersion
from loot_api import GameType from loot import GameType
from loot_api import SimpleMessage from loot import SimpleMessage
from loot_api import MessageType from loot import MessageType
from loot_api import PluginCleanliness from loot import PluginCleanliness
from loot_api import create_game_handle from loot import create_game_handle
from loot_api import is_compatible from loot import is_compatible
from loot_api import set_logging_callback from loot import set_logging_callback
from loot_api import initialise_locale
def logging_callback(level, message): def logging_callback(level, message):
pass print(level, message)
set_logging_callback(logging_callback) set_logging_callback(logging_callback)
initialise_locale()
class GameFixture(unittest.TestCase): class GameFixture(unittest.TestCase):
game_path = os.path.join(u'.', u'Oblivion') game_path = os.path.join(u'.', u'Oblivion')
local_path = os.path.join(u'.', u'local') local_path = os.path.join(u'.', u'local')
master_filename = u'Oblivion.esm'
def setUp(self): def setUp(self):
data_path = os.path.join(self.game_path, 'Data') open(self.master_file_path(), 'a').close()
master_file = os.path.join(data_path, 'Oblivion.esm')
os.makedirs(data_path) if not os.path.exists(self.local_path):
open(master_file, 'a').close() os.makedirs(self.local_path)
os.makedirs(self.local_path)
def tearDown(self): def tearDown(self):
shutil.rmtree(self.game_path) os.remove(self.master_file_path())
shutil.rmtree(self.local_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): class TestLootApi(GameFixture):
def test_is_compatible(self): def test_is_compatible(self):
self.assertFalse(is_compatible(0, 9, 0)) self.assertFalse(is_compatible(0, 9, 0))
self.assertTrue(is_compatible(0, 14, 0)) self.assertTrue(is_compatible(0, 15, 0))
def test_version(self): def test_version(self):
self.assertEqual(Version.major, 0) self.assertEqual(Version.major, 0)
self.assertEqual(Version.minor, 14) self.assertEqual(Version.minor, 15)
self.assertEqual(Version.patch, 5) self.assertEqual(Version.patch, 0)
self.assertNotEqual(Version.revision, u'') self.assertNotEqual(Version.revision, u'')
self.assertEqual(Version.string(), "0.14.5") self.assertEqual(Version.string(), "0.15.0")
def test_wrapper_version(self): def test_wrapper_version(self):
self.assertEqual(WrapperVersion.major, 4) self.assertEqual(WrapperVersion.major, 4)
@@ -68,6 +67,61 @@ class TestLootApi(GameFixture):
db = game.get_database() db = game.get_database()
self.assertNotEqual(db, None) 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): class TestDatabaseInterface(GameFixture):
masterlist_path = os.path.join(os.path.dirname(__file__), u'masterlist.yaml') masterlist_path = os.path.join(os.path.dirname(__file__), u'masterlist.yaml')