Skip to content

Commit

Permalink
[REF] Dynamically load python, thus become more portable.
Browse files Browse the repository at this point in the history
  • Loading branch information
katyukha committed Sep 10, 2023
1 parent 23884b4 commit ffaf9e0
Show file tree
Hide file tree
Showing 8 changed files with 462 additions and 85 deletions.
2 changes: 2 additions & 0 deletions dub.selections.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"fileVersion": 1,
"versions": {
"bindbc-common": "~master",
"bindbc-loader": "1.1.2",
"cachetools": "0.3.1",
"color": "0.0.9",
"colored": "0.0.31",
Expand Down
30 changes: 28 additions & 2 deletions subpackages/cli/source/odood/cli/commands/addons.d
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,36 @@ class CommandAddonsList: OdoodCommand {
];
foreach(field; fields) {
switch(field) {
case "name":
row ~= [addon.manifest.name];
break;
case "version":
row ~= [addon.manifest.module_version.toString];
break;
case "author":
row ~= [addon.manifest.author];
break;
case "category":
row ~= [addon.manifest.category];
break;
case "license":
row ~= [addon.manifest.license];
break;
case "maintainer":
row ~= [addon.manifest.maintainer];
break;
case "auto_install":
row ~= [addon.manifest.auto_install.to!string];
break;
case "application":
row ~= [addon.manifest.application.to!string];
break;
case "installable":
row ~= [addon.manifest.installable.to!string];
break;
case "tags":
row ~= [addon.manifest.tags.join(", ")];
break;
case "price":
if (addon.manifest.price.is_set)
row ~= [
Expand All @@ -212,8 +239,7 @@ class CommandAddonsList: OdoodCommand {
row ~= ["", ""];
break;
default:
row ~= [addon.manifest[field]];
break;
throw new OdoodCLIException("Unsupported manifest field %s".format(field));
}
}
return row;
Expand Down
4 changes: 2 additions & 2 deletions subpackages/lib/source/odood/lib/odoo/test.d
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ struct OdooTestRunner {

/// Add new module to test run
auto ref addModule(in OdooAddon addon) {
if (!addon.getManifest.installable) {
if (!addon.manifest.installable) {
warningf("Addon %s is not installable. Skipping", addon.name);
return this;
}
Expand All @@ -358,7 +358,7 @@ struct OdooTestRunner {

/// Add new additional module to install before test
auto ref addAdditionalModule(in OdooAddon addon) {
if (!addon.getManifest.installable) {
if (!addon.manifest.installable) {
warningf("Additional addon %s is not installable. Skipping", addon.name);
return this;
}
Expand Down
2 changes: 2 additions & 0 deletions subpackages/utils/dub.sdl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ dependency "theprocess" version=">=0.0.5"
dependency "zipper" version=">=0.0.3"
dependency "semver" version=">=0.4.0"
dependency "pyd" version=">=0.14.4"
dependency "bindbc-loader" version="~>1.1.2"
dependency "bindbc-common" version="~master"

targetPath "build"
targetType "library"
Expand Down
16 changes: 3 additions & 13 deletions subpackages/utils/source/odood/utils/addons/addon.d
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ private import odood.utils.addons.addon_manifest;
final class OdooAddon {
private immutable string _name;
private immutable Path _path;
private immutable Path _manifest_path;
private Nullable!OdooAddonManifest _manifest;
private OdooAddonManifest _manifest;

@disable this();

Expand All @@ -32,10 +31,9 @@ final class OdooAddon {
* path = Path to addon on filesystem
**/
this(in Path path) {
// TODO: there is no need to specify name here. It have to be computed based on path.
this._path = path.toAbsolute;
this._name = _path.baseName;
this._manifest_path = getAddonManifestPath(_path).get;
this._manifest = parseOdooManifest(getAddonManifestPath(_path).get);
}

/// name of the addon
Expand All @@ -45,15 +43,7 @@ final class OdooAddon {
auto path() const => _path;

/// module manifest
auto manifest() {
if (_manifest.isNull)
_manifest = OdooAddonManifest(_manifest_path).nullable;

return _manifest.get;
}

/// Get module manifest
auto getManifest() const => OdooAddonManifest(_manifest_path);
auto manifest() const => _manifest;

/// Addons are comparable by name
pure nothrow int opCmp(in OdooAddon other) const {
Expand Down
221 changes: 153 additions & 68 deletions subpackages/utils/source/odood/utils/addons/addon_manifest.d
Original file line number Diff line number Diff line change
@@ -1,98 +1,183 @@
module odood.utils.addons.addon_manifest;

private import std.typecons: Nullable, nullable, tuple;
private import std.conv: to;
private import std.format;
private import std.string;
private import std.typecons;

private import pyd.embedded: py_eval;
private import pyd.pydobject: PydObject;
private import pyd.make_object: PydConversionException;
private import thepath;

private import thepath: Path;
private import odood.utils.tipy;
private import odood.utils.tipy.python;

private import odood.utils.addons.addon_version;


/** Struct designed to read addons manifest
**/
struct OdooAddonManifest {
private PydObject _manifest;

this(in Path path) {
_manifest = py_eval(path.readFileText);
}

/// Allows to access manifest as pyd object
auto raw_manifest() {
return _manifest;
}

/// Is addon installable
bool installable() {
return _manifest.get("installable", true).to_d!bool;
}

/// Is this addon application
bool application() {
return _manifest.get("application", false).to_d!bool;
}

/** Price info for this addon
/** Use separate struct to handle prices
* The default currency is EUR
*
* Returns:
* tuple with following fields:
* - currency
* - price
* - is_set
* Additionally, this struct contains field is_set, that determines
* if price was set in manifest or not (even if price is 0).
**/
auto price() {
string currency = _manifest.get("currency","EUR").to_d!string;
private struct ManifestPrice {
string currency;
float price;
if (_manifest.has_key("price")) {
try
price = _manifest["price"].to_d!float;
catch (PydConversionException)
price = _manifest["price"].to_d!(string).to!float;
return tuple!(
"currency", "price", "is_set"
)(currency, price, true);
bool is_set = false;

string toString() const {
if (is_set)
return "%s %s".format(price, currency);
return "";
}
return tuple!(
"currency", "price", "is_set"
)(currency, price, false);
}

/// Return list of dependencies of addon
string[] dependencies() {
if (_manifest.has_key("depends"))
return _manifest["depends"].to_d!(string[]);
return [];
}
string name;
OdooAddonVersion module_version = OdooAddonVersion("1.0");
string author;
string category;
string description;
string license;
string maintainer;

bool auto_install=false;
bool application=false;
bool installable=true;

// Dependencies
string[] dependencies;
string[] python_dependencies;
string[] bin_dependencies;

// CR&D Extensions
string[] tags;

/// Return list of python dependencies
string[] python_dependencies() {
if (!_manifest.has_key("external_dependencies"))
return [];
if (!_manifest["external_dependencies"].has_key("python"))
return [];
return _manifest["external_dependencies"]["python"].to_d!(string[]);
ManifestPrice price;

/// Return string representation of manifest
string toString() const {
return "AddonManifest: %s (%s)".format(name, module_version);
}
}

/// Returns parsed module version
auto module_version() {
// If version is not specified, then return "1.0"
return OdooAddonVersion(_manifest.get("version", "1.0").to_d!string);
/** Parse Odoo manifest file
**/
auto parseOdooManifest(in string manifest_content) {
OdooAddonManifest manifest;

auto parsed = callPyFunc(_fn_literal_eval, manifest_content);
scope(exit) Py_DecRef(parsed);

// PyDict_GetItemString returns borrowed reference,
// thus there is no need to call Py_DecRef from our side
if (auto val = PyDict_GetItemString(parsed, "name".toStringz))
manifest.name = val.convertPyToD!string;
if (auto val = PyDict_GetItemString(parsed, "version".toStringz))
manifest.module_version = OdooAddonVersion(val.convertPyToD!string);
if (auto val = PyDict_GetItemString(parsed, "author".toStringz))
manifest.author = val.convertPyToD!string;
if (auto val = PyDict_GetItemString(parsed, "category".toStringz))
manifest.category = val.convertPyToD!string;
if (auto val = PyDict_GetItemString(parsed, "description".toStringz))
manifest.description = val.convertPyToD!string;
if (auto val = PyDict_GetItemString(parsed, "license".toStringz))
manifest.license = val.convertPyToD!string;
if (auto val = PyDict_GetItemString(parsed, "maintainer".toStringz))
manifest.maintainer = val.convertPyToD!string;

if (auto val = PyDict_GetItemString(parsed, "auto_install".toStringz))
manifest.auto_install = val.convertPyToD!bool;
if (auto val = PyDict_GetItemString(parsed, "application".toStringz))
manifest.application = val.convertPyToD!bool;
if (auto val = PyDict_GetItemString(parsed, "installable".toStringz))
manifest.installable = val.convertPyToD!bool;

if (auto val = PyDict_GetItemString(parsed, "depends".toStringz))
manifest.dependencies = val.convertPyToD!(string[]);
if (auto external_deps = PyDict_GetItemString(parsed, "external_dependencies".toStringz)) {
if (auto val = PyDict_GetItemString(external_deps, "python".toStringz))
manifest.python_dependencies = val.convertPyToD!(string[]);
if (auto val = PyDict_GetItemString(external_deps, "bin".toStringz))
manifest.bin_dependencies = val.convertPyToD!(string[]);
}

/// Access manifest item as string:
string opIndex(in string index) {
return _manifest.get(index, "").to_d!string;
if (auto val = PyDict_GetItemString(parsed, "tags".toStringz))
manifest.tags = val.convertPyToD!(string[]);


if (auto py_price = PyDict_GetItemString(parsed, "price".toStringz)) {
manifest.price.price = py_price.convertPyToD!float;

if (auto py_currency = PyDict_GetItemString(parsed, "currency".toStringz)) {
manifest.price.currency = py_currency.convertPyToD!string;
} else {
manifest.price.currency = "EUR";
}

manifest.price.is_set = true;
}

return manifest;
}

/// ditto
auto parseOdooManifest(in Path path) {
return parseOdooManifest(path.readFileText);
}

// Module level link to ast module
private PyObject* _fn_literal_eval;

// Initialize pyd as early as possible.
// Initialize python interpreter (import ast.literal_eval)
shared static this() {
import pyd.def: py_init;
py_init();
loadPyLib;

Py_Initialize();

auto mod_ast = PyImport_ImportModule("ast");
scope(exit) Py_DecRef(mod_ast);

// Save function literal_eval from ast on module level
_fn_literal_eval = PyObject_GetAttrString(
mod_ast, "literal_eval".toStringz
).pyEnforce;


}

// Finalize python interpreter (do clean up)
shared static ~this() {
if (_fn_literal_eval) Py_DecRef(_fn_literal_eval);
Py_Finalize();
}


// Tests
unittest {
auto manifest = parseOdooManifest(`{
'name': "A Module",
'version': '1.0',
'depends': ['base'],
'author': "Author Name",
'category': 'Category',
'description': """
Description text
""",
# data files always loaded at installation
'data': [
'views/mymodule_view.xml',
],
# data files containing optionally loaded demonstration data
'demo': [
'demo/demo_data.xml',
],
}`);

assert(manifest.name == "A Module");
assert(manifest.module_version.isStandard == false);
assert(manifest.module_version.toString == "1.0");
assert(manifest.module_version.rawVersion == "1.0");
assert(manifest.dependencies == ["base"]);
}
Loading

0 comments on commit ffaf9e0

Please sign in to comment.