Addon Manager: DevMode content implementation

This commit is contained in:
Chris Hennes
2022-09-07 19:00:26 -05:00
parent 7a17106776
commit cb1f6bffa8
11 changed files with 1658 additions and 226 deletions

View File

@@ -207,7 +207,7 @@ std::string Metadata::classname() const
return _classname;
}
boost::filesystem::path App::Metadata::subdirectory() const
boost::filesystem::path Metadata::subdirectory() const
{
return _subdirectory;
}
@@ -217,12 +217,12 @@ std::vector<fs::path> Metadata::file() const
return _file;
}
Meta::Version App::Metadata::freecadmin() const
Meta::Version Metadata::freecadmin() const
{
return _freecadmin;
}
Meta::Version App::Metadata::freecadmax() const
Meta::Version Metadata::freecadmax() const
{
return _freecadmax;
}
@@ -315,7 +315,7 @@ void Metadata::setClassname(const std::string& name)
_classname = name;
}
void App::Metadata::setSubdirectory(const boost::filesystem::path& path)
void Metadata::setSubdirectory(const boost::filesystem::path& path)
{
_subdirectory = path;
}
@@ -330,22 +330,22 @@ void Metadata::addContentItem(const std::string& tag, const Metadata& item)
_content.insert(std::make_pair(tag, item));
}
void App::Metadata::setFreeCADMin(const Meta::Version& version)
void Metadata::setFreeCADMin(const Meta::Version& version)
{
_freecadmin = version;
}
void App::Metadata::setFreeCADMax(const Meta::Version& version)
void Metadata::setFreeCADMax(const Meta::Version& version)
{
_freecadmax = version;
}
void App::Metadata::addGenericMetadata(const std::string& tag, const Meta::GenericMetadata& genericMetadata)
void Metadata::addGenericMetadata(const std::string& tag, const Meta::GenericMetadata& genericMetadata)
{
_genericMetadata.insert(std::make_pair(tag, genericMetadata));
}
void App::Metadata::removeContentItem(const std::string& tag, const std::string& itemName)
void Metadata::removeContentItem(const std::string& tag, const std::string& itemName)
{
auto tagRange = _content.equal_range(tag);
auto foundItem = std::find_if(tagRange.first, tagRange.second, [&itemName](const auto& check) -> bool { return itemName == check.second.name(); });
@@ -353,6 +353,119 @@ void App::Metadata::removeContentItem(const std::string& tag, const std::string&
_content.erase(foundItem);
}
void Metadata::removeMaintainer(const Meta::Contact &maintainer)
{
auto new_end = std::remove(_maintainer.begin(), _maintainer.end(), maintainer);
_maintainer.erase(new_end, _maintainer.end());
}
void Metadata::removeLicense(const Meta::License &license)
{
auto new_end = std::remove(_license.begin(), _license.end(), license);
_license.erase(new_end, _license.end());
}
void Metadata::removeUrl(const Meta::Url &url)
{
auto new_end = std::remove(_url.begin(), _url.end(), url);
_url.erase(new_end, _url.end());
}
void Metadata::removeAuthor(const Meta::Contact &author)
{
auto new_end = std::remove(_author.begin(), _author.end(), author);
_author.erase(new_end, _author.end());
}
void Metadata::removeDepend(const Meta::Dependency &dep)
{
bool found = false;
for (const auto& check : _depend) {
if (dep == check)
found = true;
}
if (!found)
throw Base::RuntimeError("No match found for dependency to remove");
auto new_end = std::remove(_depend.begin(), _depend.end(), dep);
_depend.erase(new_end, _depend.end());
}
void Metadata::removeConflict(const Meta::Dependency &dep)
{
auto new_end = std::remove(_conflict.begin(), _conflict.end(), dep);
_conflict.erase(new_end, _conflict.end());
}
void Metadata::removeReplace(const Meta::Dependency &dep)
{
auto new_end = std::remove(_replace.begin(), _replace.end(), dep);
_replace.erase(new_end, _replace.end());
}
void Metadata::removeTag(const std::string &tag)
{
auto new_end = std::remove(_tag.begin(), _tag.end(), tag);
_tag.erase(new_end, _tag.end());
}
void Metadata::removeFile(const boost::filesystem::path &path)
{
auto new_end = std::remove(_file.begin(), _file.end(), path);
_file.erase(new_end, _file.end());
}
void Metadata::clearContent()
{
_content.clear();
}
void Metadata::clearMaintainer()
{
_maintainer.clear();
}
void Metadata::clearLicense()
{
_license.clear();
}
void Metadata::clearUrl()
{
_url.clear();
}
void Metadata::clearAuthor()
{
_author.clear();
}
void Metadata::clearDepend()
{
_depend.clear();
}
void Metadata::clearConflict()
{
_conflict.clear();
}
void Metadata::clearReplace()
{
_replace.clear();
}
void Metadata::clearTag()
{
_tag.clear();
}
void Metadata::clearFile()
{
_file.clear();
}
DOMElement* appendSimpleXMLNode(DOMElement* baseNode, const std::string& nodeName, const std::string& nodeContents)
{
@@ -376,6 +489,36 @@ void addAttribute(DOMElement* node, const std::string& key, const std::string& v
node->setAttribute(XUTF8Str(key.c_str()).unicodeForm(), XUTF8Str(value.c_str()).unicodeForm());
}
void addAttribute(DOMElement *node, const std::string &key, bool value)
{
if (value)
node->setAttribute(XUTF8Str(key.c_str()).unicodeForm(), XUTF8Str("True").unicodeForm());
else
node->setAttribute(XUTF8Str(key.c_str()).unicodeForm(), XUTF8Str("False").unicodeForm());
}
void addAttribute(DOMElement *node, const std::string &key, Meta::DependencyType value)
{
// Someday we should be able to change this to use reflection, but it's not yet
// available (using C++17)
std::string stringified("automatic");
switch (value) {
case Meta::DependencyType::automatic:
stringified = "automatic";
break;
case Meta::DependencyType::internal:
stringified = "internal";
break;
case Meta::DependencyType::addon:
stringified = "addon";
break;
case Meta::DependencyType::python:
stringified = "python";
break;
}
node->setAttribute(XUTF8Str(key.c_str()).unicodeForm(), XUTF8Str(stringified.c_str()).unicodeForm());
}
void addDependencyNode(DOMElement* root, const std::string& name, const Meta::Dependency& depend)
{
auto element = appendSimpleXMLNode(root, name, depend.package);
@@ -386,6 +529,8 @@ void addDependencyNode(DOMElement* root, const std::string& name, const Meta::De
addAttribute(element, "version_gte", depend.version_gte);
addAttribute(element, "version_gt", depend.version_gt);
addAttribute(element, "condition", depend.condition);
addAttribute(element, "optional", depend.optional);
addAttribute(element, "type", depend.dependencyType);
}
}
@@ -489,7 +634,7 @@ bool Metadata::satisfies(const Meta::Dependency& dep)
return true;
}
bool App::Metadata::supportsCurrentFreeCAD() const
bool Metadata::supportsCurrentFreeCAD() const
{
static auto fcVersion = Meta::Version();
if (fcVersion == Meta::Version()) {
@@ -683,6 +828,11 @@ Meta::Contact::Contact(const XERCES_CPP_NAMESPACE::DOMElement* e)
email = StrXUTF8(emailAttribute).str;
}
bool App::Meta::Contact::operator==(const Contact &rhs) const
{
return name==rhs.name && email==rhs.email;
}
Meta::License::License(const std::string& name, fs::path file) :
name(name),
file(file)
@@ -699,7 +849,16 @@ Meta::License::License(const XERCES_CPP_NAMESPACE::DOMElement* e)
name = StrXUTF8(e->getTextContent()).str;
}
Meta::Url::Url(const std::string& location, UrlType type) :
bool App::Meta::License::operator==(const License &rhs) const
{
return name==rhs.name && file==rhs.file;
}
App::Meta::Url::Url() : location(""), type(App::Meta::UrlType::website)
{
}
Meta::Url::Url(const std::string &location, UrlType type) :
location(location),
type(type)
{
@@ -728,7 +887,22 @@ Meta::Url::Url(const XERCES_CPP_NAMESPACE::DOMElement* e)
location = StrXUTF8(e->getTextContent()).str;
}
Meta::Dependency::Dependency(const XERCES_CPP_NAMESPACE::DOMElement* e)
bool App::Meta::Url::operator==(const Url &rhs) const
{
if (type == UrlType::repository && branch != rhs.branch)
return false;
return type==rhs.type && location==rhs.location;
}
App::Meta::Dependency::Dependency() : optional(false), dependencyType(App::Meta::DependencyType::automatic)
{
}
App::Meta::Dependency::Dependency(const std::string &pkg) : package(pkg), optional(false), dependencyType(App::Meta::DependencyType::automatic)
{
}
Meta::Dependency::Dependency(const XERCES_CPP_NAMESPACE::DOMElement *e)
{
version_lt = StrXUTF8(e->getAttribute(XUTF8Str("version_lt").unicodeForm())).str;
version_lte = StrXUTF8(e->getAttribute(XUTF8Str("version_lte").unicodeForm())).str;
@@ -736,10 +910,41 @@ Meta::Dependency::Dependency(const XERCES_CPP_NAMESPACE::DOMElement* e)
version_gte = StrXUTF8(e->getAttribute(XUTF8Str("version_gte").unicodeForm())).str;
version_gt = StrXUTF8(e->getAttribute(XUTF8Str("version_gt").unicodeForm())).str;
condition = StrXUTF8(e->getAttribute(XUTF8Str("condition").unicodeForm())).str;
std::string opt_string = StrXUTF8(e->getAttribute(XUTF8Str("optional").unicodeForm())).str;
if (opt_string == "true" || opt_string == "True") // Support Python capitalization in this one case...
optional = true;
else
optional = false;
std::string type_string = StrXUTF8(e->getAttribute(XUTF8Str("type").unicodeForm())).str;
if (type_string == "automatic" || type_string == "")
dependencyType = Meta::DependencyType::automatic;
else if (type_string == "addon")
dependencyType = Meta::DependencyType::addon;
else if (type_string == "internal")
dependencyType = Meta::DependencyType::internal;
else if (type_string == "python")
dependencyType = Meta::DependencyType::python;
else {
auto message = std::string("Invalid dependency type \"") + type_string + "\"";
throw Base::XMLBaseException(message);
}
package = StrXUTF8(e->getTextContent()).str;
}
bool App::Meta::Dependency::operator==(const Dependency &rhs) const
{
return package == rhs.package &&
version_lt == rhs.version_lt &&
version_lte == rhs.version_lte &&
version_eq == rhs.version_eq &&
version_gte == rhs.version_gte &&
version_gt == rhs.version_gt &&
condition == rhs.condition &&
optional == rhs.optional &&
dependencyType == rhs.dependencyType;
}
Meta::Version::Version() :
major(0),
minor(0),

View File

@@ -50,6 +50,7 @@ namespace App {
explicit Contact(const XERCES_CPP_NAMESPACE::DOMElement* e);
std::string name; //< Contact name - required
std::string email; //< Contact email - may be optional
bool operator==(const Contact &rhs) const;
};
/**
@@ -65,6 +66,7 @@ namespace App {
explicit License(const XERCES_CPP_NAMESPACE::DOMElement* e);
std::string name; //< Short name of license, e.g. "LGPL2", "MIT", "Mozilla Public License", etc.
boost::filesystem::path file; //< Optional path to the license file, relative to the XML file's location
bool operator==(const License &rhs) const;
};
enum class UrlType {
@@ -80,12 +82,13 @@ namespace App {
* \brief A URL, including type information (e.g. website, repository, or bugtracker, in package.xml)
*/
struct AppExport Url {
Url() = default;
Url();
Url(const std::string& location, UrlType type);
explicit Url(const XERCES_CPP_NAMESPACE::DOMElement* e);
std::string location; //< The actual URL, including protocol
UrlType type; //< What kind of URL this is
std::string branch; //< If it's a repository, which branch to use
bool operator==(const Url &rhs) const;
};
/**
@@ -112,12 +115,25 @@ namespace App {
bool operator!=(const Version&) const;
};
/**
* \enum DependencyType
* The type of dependency.
*/
enum class DependencyType
{
automatic,
internal,
addon,
python
};
/**
* \struct Dependency
* \brief Another package that this package depends on, conflicts with, or replaces
*/
struct AppExport Dependency {
Dependency() = default;
Dependency();
explicit Dependency(const std::string &pkg);
explicit Dependency(const XERCES_CPP_NAMESPACE::DOMElement* e);
std::string package; //< Required: must exactly match the contents of the "name" element in the referenced package's package.xml file.
std::string version_lt; //< Optional: The dependency to the package is restricted to versions less than the stated version number.
@@ -126,6 +142,9 @@ namespace App {
std::string version_gte; //< Optional: The dependency to the package is restricted to versions greater or equal than the stated version number.
std::string version_gt; //< Optional: The dependency to the package is restricted to versions greater than the stated version number.
std::string condition; //< Optional: Conditional expression as documented in REP149.
bool optional;//< Optional: Whether this dependency is considered "optional"
DependencyType dependencyType; //< Optional: defaults to "automatic"
bool operator==(const Dependency &rhs) const;
};
/**
@@ -238,25 +257,46 @@ namespace App {
void setName(const std::string& name);
void setVersion(const Meta::Version& version);
void setDescription(const std::string& description);
void addMaintainer(const Meta::Contact& maintainer);
void addLicense(const Meta::License& license);
void addUrl(const Meta::Url& url);
void addAuthor(const Meta::Contact& author);
void addDepend(const Meta::Dependency& dep);
void addConflict(const Meta::Dependency& dep);
void addReplace(const Meta::Dependency& dep);
void addTag(const std::string& tag);
void addMaintainer(const Meta::Contact &maintainer);
void addLicense(const Meta::License &license);
void addUrl(const Meta::Url &url);
void addAuthor(const Meta::Contact &author);
void addDepend(const Meta::Dependency &dep);
void addConflict(const Meta::Dependency &dep);
void addReplace(const Meta::Dependency &dep);
void addTag(const std::string &tag);
void setIcon(const boost::filesystem::path& path);
void setClassname(const std::string& name);
void setSubdirectory(const boost::filesystem::path& path);
void addFile(const boost::filesystem::path& path);
void addContentItem(const std::string& tag, const Metadata& item);
void addFile(const boost::filesystem::path &path);
void addContentItem(const std::string &tag, const Metadata &item);
void setFreeCADMin(const Meta::Version& version);
void setFreeCADMax(const Meta::Version& version);
void addGenericMetadata(const std::string& tag, const Meta::GenericMetadata& genericMetadata);
void addGenericMetadata(const std::string &tag, const Meta::GenericMetadata &genericMetadata);
// Deleters (work in progress...)
void removeContentItem(const std::string& tag, const std::string& itemName);
// Deleters
void removeContentItem(const std::string &tag, const std::string &itemName);
void removeMaintainer(const Meta::Contact &maintainer);
void removeLicense(const Meta::License &license);
void removeUrl(const Meta::Url &url);
void removeAuthor(const Meta::Contact &author);
void removeDepend(const Meta::Dependency &dep);
void removeConflict(const Meta::Dependency &dep);
void removeReplace(const Meta::Dependency &dep);
void removeTag(const std::string &tag);
void removeFile(const boost::filesystem::path &path);
// Utility functions to clear lists
void clearContent();
void clearMaintainer();
void clearLicense();
void clearUrl();
void clearAuthor();
void clearDepend();
void clearConflict();
void clearReplace();
void clearTag();
void clearFile();
/**
* Write the metadata to an XML file

View File

@@ -14,11 +14,12 @@
RichCompare="false"
FatherNamespace="Base">
<Documentation>
<Author Licence="LGPL" Name="Chris Hennes" EMail="chennes@pioneerlibrarysystem.org" />
<UserDocu>App.Metadata class.\n
A Metadata object reads an XML-formatted package metadata file and provides
read-only access to its contents.\n
read and write access to its contents.\n
The following constructors are supported:\n
Metadata()
Empty constructor.\n
@@ -31,37 +32,37 @@ file : str\n XML file name.</UserDocu>
<DeveloperDocu>Metadata</DeveloperDocu>
</Documentation>
<Attribute Name="Name" ReadOnly="true">
<Attribute Name="Name">
<Documentation>
<UserDocu>String representing the name of this item.</UserDocu>
</Documentation>
<Parameter Name="Name" Type="Object" />
</Attribute>
<Attribute Name="Version" ReadOnly="true">
<Attribute Name="Version">
<Documentation>
<UserDocu>String representing the version of this item in semantic triplet format.</UserDocu>
</Documentation>
<Parameter Name="Version" Type="Object" />
</Attribute>
<Attribute Name="Description" ReadOnly="true">
<Attribute Name="Description">
<Documentation>
<UserDocu>String representing the description of this item (text only, no markup allowed).</UserDocu>
</Documentation>
<Parameter Name="Description" Type="Object" />
</Attribute>
<Attribute Name="Maintainer" ReadOnly="true">
<Attribute Name="Maintainer">
<Documentation>
<UserDocu>List of maintainer objects with 'name' and 'email' string attributes.</UserDocu>
</Documentation>
<Parameter Name="Maintainer" Type="Object" />
</Attribute>
<Attribute Name="License" ReadOnly="true">
<Attribute Name="License">
<Documentation>
<UserDocu>List of applicable licenses as objects with 'name' and 'file' string attributes.</UserDocu>
</Documentation>
<Parameter Name="License" Type="Object" />
</Attribute>
<Attribute Name="Urls" ReadOnly="true">
<Attribute Name="Urls">
<Documentation>
<UserDocu>List of URLs as objects with 'location' and 'type' string attributes, where type
is one of:
@@ -73,14 +74,14 @@ is one of:
</Documentation>
<Parameter Name="Urls" Type="Object" />
</Attribute>
<Attribute Name="Author" ReadOnly="true">
<Attribute Name="Author">
<Documentation>
<UserDocu>List of author objects, each with a 'name' and a (potentially empty) 'email'
string attribute.</UserDocu>
</Documentation>
<Parameter Name="Author" Type="Object" />
</Attribute>
<Attribute Name="Depend" ReadOnly="true">
<Attribute Name="Depend">
<Documentation>
<UserDocu>List of dependencies, as objects with the following attributes:
* package
@@ -106,53 +107,53 @@ string attribute.</UserDocu>
</Documentation>
<Parameter Name="Depend" Type="Object" />
</Attribute>
<Attribute Name="Conflict" ReadOnly="true">
<Attribute Name="Conflict">
<Documentation>
<UserDocu>List of conflicts, format identical to dependencies.</UserDocu>
</Documentation>
<Parameter Name="Conflict" Type="Object" />
</Attribute>
<Attribute Name="Replace" ReadOnly="true">
<Attribute Name="Replace">
<Documentation>
<UserDocu>List of things this item is considered by its author to replace. The format is
identical to dependencies.</UserDocu>
</Documentation>
<Parameter Name="Replace" Type="Object" />
</Attribute>
<Attribute Name="Tag" ReadOnly="true">
<Attribute Name="Tag">
<Documentation>
<UserDocu>List of strings.</UserDocu>
</Documentation>
<Parameter Name="Tag" Type="Object" />
</Attribute>
<Attribute Name="Icon" ReadOnly="true">
<Attribute Name="Icon">
<Documentation>
<UserDocu>Relative path to an icon file.</UserDocu>
</Documentation>
<Parameter Name="Icon" Type="Object" />
</Attribute>
<Attribute Name="Classname" ReadOnly="true">
<Attribute Name="Classname">
<Documentation>
<UserDocu>String representing the name of the main Python class this item
creates/represents.</UserDocu>
</Documentation>
<Parameter Name="Classname" Type="Object" />
</Attribute>
<Attribute Name="Subdirectory" ReadOnly="true">
<Attribute Name="Subdirectory">
<Documentation>
<UserDocu>String representing the name of the subdirectory this content item is located in.
If empty, the item is in a directory named the same as the content item.</UserDocu>
</Documentation>
<Parameter Name="Subdirectory" Type="Object" />
</Attribute>
<Attribute Name="File" ReadOnly="true">
<Attribute Name="File">
<Documentation>
<UserDocu>List of files associated with this item.
The meaning of each file is implementation-defined.</UserDocu>
</Documentation>
<Parameter Name="File" Type="Object" />
</Attribute>
<Attribute Name="Content" ReadOnly="true">
<Attribute Name="Content">
<Documentation>
<UserDocu>Dictionary of lists of content items: defined recursively, each item is itself
a Metadata object.
@@ -219,6 +220,151 @@ dictionary of strings, 'attributes'. They represent unrecognized simple XML tags
in the metadata file.</UserDocu>
</Documentation>
</Methode>
<Methode Name="addContentItem">
<Documentation>
<UserDocu>addContentItem(content_type,metadata)\n
Add a new content item of type 'content_type' with metadata 'metadata'. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeContentItem">
<Documentation>
<UserDocu>removeContentItem(content_type,name)\n
Remove the content item of type 'content_type' with name 'name'. </UserDocu>
</Documentation>
</Methode>
<Methode Name="addMaintainer">
<Documentation>
<UserDocu>addMaintainer(name, email)\n
Add a new Maintainer. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeMaintainer">
<Documentation>
<UserDocu>removeMaintainer(name, email)\n
Remove the Maintainer. </UserDocu>
</Documentation>
</Methode>
<Methode Name="addLicense">
<Documentation>
<UserDocu>addLicense(short_code,path)\n
Add a new License. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeLicense">
<Documentation>
<UserDocu>removeLicense(short_code)\n
Remove the License. </UserDocu>
</Documentation>
</Methode>
<Methode Name="addUrl">
<Documentation>
<UserDocu>addUrl(url_type,url,branch)\n
Add a new Url or type 'url_type' (which should be one of 'repository', 'readme',\n
'bugtracker', 'documentation', or 'webpage') If type is 'repository' you\n
must also specify the 'branch' parameter. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeUrl">
<Documentation>
<UserDocu>removeUrl(url_type,url)\n
Remove the Url. </UserDocu>
</Documentation>
</Methode>
<Methode Name="addAuthor">
<Documentation>
<UserDocu>addAuthor(name, email)\n
Add a new Author with name 'name', and optionally email 'email'. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeAuthor">
<Documentation>
<UserDocu>removeAuthor(name, email)\n
Remove the Author. </UserDocu>
</Documentation>
</Methode>
<Methode Name="addDepend">
<Documentation>
<UserDocu>addDepend(name, kind, optional)\n
Add a new Dependency on package 'name' of kind 'kind' (optional, one of 'auto' (the default),\n
'internal', 'addon', or 'python'). </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeDepend">
<Documentation>
<UserDocu>removeDepend(name, kind)\n
Remove the Dependency on package 'name' of kind 'kind' (optional - if unspecified any\n
matching name is removed). </UserDocu>
</Documentation>
</Methode>
<Methode Name="addConflict">
<Documentation>
<UserDocu>addConflict(name, kind)\n
Add a new Conflict. See documentation for addDepend(). </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeConflict">
<Documentation>
<UserDocu>removeConflict(name, kind)\n
Remove the Conflict. See documentation for removeDepend().</UserDocu>
</Documentation>
</Methode>
<Methode Name="addReplace">
<Documentation>
<UserDocu>addReplace(name)\n
Add a new Replace. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeReplace">
<Documentation>
<UserDocu>removeReplace(name)\n
Remove the Replace. </UserDocu>
</Documentation>
</Methode>
<Methode Name="addTag">
<Documentation>
<UserDocu>addTag(tag)\n
Add a new Tag. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeTag">
<Documentation>
<UserDocu>removeTag(tag)\n
Remove the Tag. </UserDocu>
</Documentation>
</Methode>
<Methode Name="addFile">
<Documentation>
<UserDocu>addFile(filename)\n
Add a new File. </UserDocu>
</Documentation>
</Methode>
<Methode Name="removeFile">
<Documentation>
<UserDocu>removeFile(filename)\n
Remove the File. </UserDocu>
</Documentation>
</Methode>
<ClassDeclarations>
public:

View File

@@ -50,13 +50,13 @@ std::string MetadataPy::representation() const
return str.str();
}
PyObject* MetadataPy::PyMake(struct _typeobject*, PyObject*, PyObject*) // Python wrapper
PyObject *MetadataPy::PyMake(struct _typeobject *, PyObject *, PyObject *)// Python wrapper
{
return new MetadataPy(nullptr);
}
// constructor method
int MetadataPy::PyInit(PyObject* args, PyObject* /*kwd*/)
int MetadataPy::PyInit(PyObject *args, PyObject * /*kwd*/)
{
if (PyArg_ParseTuple(args, "")) {
setTwinPointer(new Metadata());
@@ -65,7 +65,7 @@ int MetadataPy::PyInit(PyObject* args, PyObject* /*kwd*/)
// Main class constructor -- takes a file path, loads the metadata from it
PyErr_Clear();
char* filename;
char *filename;
if (PyArg_ParseTuple(args, "et", "utf-8", &filename)) {
try {
std::string utf8Name = std::string(filename);
@@ -75,19 +75,19 @@ int MetadataPy::PyInit(PyObject* args, PyObject* /*kwd*/)
setTwinPointer(md);
return 0;
}
catch (const Base::XMLBaseException& e) {
catch (const Base::XMLBaseException &e) {
e.setPyException();
return -1;
}
catch (const XMLException& toCatch) {
char* message = XMLString::transcode(toCatch.getMessage());
catch (const XMLException &toCatch) {
char *message = XMLString::transcode(toCatch.getMessage());
std::string what = message;
XMLString::release(&message);
PyErr_SetString(Base::PyExc_FC_XMLBaseException, what.c_str());
return -1;
}
catch (const DOMException& toCatch) {
char* message = XMLString::transcode(toCatch.getMessage());
catch (const DOMException &toCatch) {
char *message = XMLString::transcode(toCatch.getMessage());
std::string what = message;
XMLString::release(&message);
PyErr_SetString(Base::PyExc_FC_XMLBaseException, what.c_str());
@@ -101,9 +101,9 @@ int MetadataPy::PyInit(PyObject* args, PyObject* /*kwd*/)
// Copy constructor
PyErr_Clear();
PyObject* o;
PyObject *o;
if (PyArg_ParseTuple(args, "O!", &(App::MetadataPy::Type), &o)) {
App::Metadata* a = static_cast<App::MetadataPy*>(o)->getMetadataPtr();
App::Metadata *a = static_cast<App::MetadataPy *>(o)->getMetadataPtr();
setTwinPointer(new Metadata(*a));
return 0;
}
@@ -117,21 +117,52 @@ Py::Object MetadataPy::getName() const
return Py::String(getMetadataPtr()->name());
}
void MetadataPy::setName(Py::Object args)
{
const char *name = nullptr;
if (!PyArg_Parse(args.ptr(), "z", &name)) {
throw Py::Exception();
}
if (name)
getMetadataPtr()->setName(name);
else
getMetadataPtr()->setName("");
}
Py::Object MetadataPy::getVersion() const
{
return Py::String(getMetadataPtr()->version().str());
}
void MetadataPy::setVersion(Py::Object args)
{
const char *name = nullptr;
if (!PyArg_Parse(args.ptr(), "z", &name))
throw Py::Exception();
if (name)
getMetadataPtr()->setVersion(App::Meta::Version(std::string(name)));
else
getMetadataPtr()->setVersion(App::Meta::Version());
}
Py::Object MetadataPy::getDescription() const
{
return Py::String(getMetadataPtr()->description());
}
void MetadataPy::setDescription(Py::Object args)
{
const char *description = nullptr;
if (!PyArg_Parse(args.ptr(), "s", &description))
throw Py::Exception();
getMetadataPtr()->setDescription(description);
}
Py::Object MetadataPy::getMaintainer() const
{
auto maintainers = getMetadataPtr()->maintainer();
Py::List pyMaintainers;
for (const auto& m : maintainers) {
for (const auto &m : maintainers) {
Py::Dict pyMaintainer;
pyMaintainer["name"] = Py::String(m.name);
pyMaintainer["email"] = Py::String(m.email);
@@ -140,11 +171,50 @@ Py::Object MetadataPy::getMaintainer() const
return pyMaintainers;
}
void MetadataPy::setMaintainer(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearMaintainer();
Py::List maintainers(list);
for (const auto &m : maintainers) {
Py::Dict pyMaintainer(m);
std::string name = pyMaintainer["name"].str();
std::string email = pyMaintainer["email"].str();
getMetadataPtr()->addMaintainer(App::Meta::Contact(name, email));
}
}
PyObject *MetadataPy::addMaintainer(PyObject *args)
{
const char *name = nullptr;
const char *email = nullptr;
if (!PyArg_ParseTuple(args, "ss", &name, &email))
throw Py::Exception();
getMetadataPtr()->addMaintainer(App::Meta::Contact(name, email));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeMaintainer(PyObject *args)
{
const char *name = nullptr;
const char *email = nullptr;
if (!PyArg_ParseTuple(args, "ss", &name, &email))
throw Py::Exception();
getMetadataPtr()->removeMaintainer(App::Meta::Contact(name, email));
Py_INCREF(Py_None);
return Py_None;
}
Py::Object MetadataPy::getAuthor() const
{
auto authors = getMetadataPtr()->author();
Py::List pyAuthors;
for (const auto& a : authors) {
for (const auto &a : authors) {
Py::Dict pyAuthor;
pyAuthor["name"] = Py::String(a.name);
pyAuthor["email"] = Py::String(a.email);
@@ -153,11 +223,49 @@ Py::Object MetadataPy::getAuthor() const
return pyAuthors;
}
void MetadataPy::setAuthor(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearAuthor();
Py::List authors(list);
for (const auto &a : authors) {
Py::Dict pyAuthor(a);
std::string name = pyAuthor["name"].str();
std::string email = pyAuthor["email"].str();
getMetadataPtr()->addAuthor(App::Meta::Contact(name, email));
}
}
PyObject *MetadataPy::addAuthor(PyObject *args)
{
const char *name = nullptr;
const char *email = nullptr;
if (!PyArg_ParseTuple(args, "ss", &name, &email))
throw Py::Exception();
getMetadataPtr()->addAuthor(App::Meta::Contact(name, email));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeAuthor(PyObject *args)
{
const char *name = nullptr;
const char *email = nullptr;
if (!PyArg_ParseTuple(args, "ss", &name, &email))
throw Py::Exception();
getMetadataPtr()->removeAuthor(App::Meta::Contact(name, email));
Py_INCREF(Py_None);
return Py_None;
}
Py::Object MetadataPy::getLicense() const
{
auto licenses = getMetadataPtr()->license();
Py::List pyLicenses;
for (const auto& lic : licenses) {
for (const auto &lic : licenses) {
Py::Dict pyLicense;
pyLicense["name"] = Py::String(lic.name);
pyLicense["file"] = Py::String(lic.file.string());
@@ -166,19 +274,57 @@ Py::Object MetadataPy::getLicense() const
return pyLicenses;
}
void MetadataPy::setLicense(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearLicense();
Py::List licenses(list);
for (const auto &l : licenses) {
Py::Dict pyLicense(l);
std::string name = pyLicense["name"].str();
std::string path = pyLicense["file"].str();
getMetadataPtr()->addLicense(App::Meta::License(name, path));
}
}
PyObject *MetadataPy::addLicense(PyObject *args)
{
const char *shortCode = nullptr;
const char *path = nullptr;
if (!PyArg_ParseTuple(args, "ss", &shortCode, &path))
throw Py::Exception();
getMetadataPtr()->addLicense(App::Meta::License(shortCode, path));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeLicense(PyObject *args)
{
const char *shortCode = nullptr;
const char *path = nullptr;
if (!PyArg_ParseTuple(args, "ss", &shortCode, &path))
throw Py::Exception();
getMetadataPtr()->removeLicense(App::Meta::License(shortCode, path));
Py_INCREF(Py_None);
return Py_None;
}
Py::Object MetadataPy::getUrls() const
{
auto urls = getMetadataPtr()->url ();
auto urls = getMetadataPtr()->url();
Py::List pyUrls;
for (const auto& url : urls) {
for (const auto &url : urls) {
Py::Dict pyUrl;
pyUrl["location"] = Py::String(url.location);
switch (url.type) {
case Meta::UrlType::website: pyUrl["type"] = Py::String("website"); break;
case Meta::UrlType::repository: pyUrl["type"] = Py::String("repository"); break;
case Meta::UrlType::bugtracker: pyUrl["type"] = Py::String("bugtracker"); break;
case Meta::UrlType::readme: pyUrl["type"] = Py::String("readme"); break;
case Meta::UrlType::documentation: pyUrl["type"] = Py::String("documentation"); break;
case Meta::UrlType::website: pyUrl["type"] = Py::String("website"); break;
case Meta::UrlType::repository: pyUrl["type"] = Py::String("repository"); break;
case Meta::UrlType::bugtracker: pyUrl["type"] = Py::String("bugtracker"); break;
case Meta::UrlType::readme: pyUrl["type"] = Py::String("readme"); break;
case Meta::UrlType::documentation: pyUrl["type"] = Py::String("documentation"); break;
}
if (url.type == Meta::UrlType::repository)
pyUrl["branch"] = Py::String(url.branch);
@@ -187,7 +333,93 @@ Py::Object MetadataPy::getUrls() const
return pyUrls;
}
Py::Object dependencyToPyObject(const Meta::Dependency& d)
void MetadataPy::setUrls(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearUrl();
Py::List urls(list);
for (const auto &url : urls) {
Py::Dict pyUrl(url);
std::string location = pyUrl["location"].str();
std::string typeAsString = pyUrl["type"].str();
std::string branch = pyUrl["branch"].str();
auto newUrl = App::Meta::Url(location, Meta::UrlType::website);
if (typeAsString == "website") {
newUrl.type = Meta::UrlType::website;
}
else if (typeAsString == "repository") {
newUrl.type = Meta::UrlType::repository;
newUrl.branch = branch;
}
else if (typeAsString == "bugtracker") {
newUrl.type = Meta::UrlType::bugtracker;
}
else if (typeAsString == "readme") {
newUrl.type = Meta::UrlType::readme;
}
else if (typeAsString == "documentation") {
newUrl.type = Meta::UrlType::documentation;
}
else {
PyErr_SetString(Base::PyExc_FC_GeneralError, "Unrecognized URL type");
return;
}
getMetadataPtr()->addUrl(newUrl);
}
}
App::Meta::Url urlFromStrings(const char* urlTypeCharStar, const char* link, const char* branch)
{
std::string urlTypeString(urlTypeCharStar);
App::Meta::UrlType urlType;
if (urlTypeString == "repository")
urlType = App::Meta::UrlType::repository;
else if (urlTypeString == "bugtracker")
urlType = App::Meta::UrlType::bugtracker;
else if (urlTypeString == "documentation")
urlType = App::Meta::UrlType::documentation;
else if (urlTypeString == "readme")
urlType = App::Meta::UrlType::readme;
else if (urlTypeString == "website")
urlType = App::Meta::UrlType::website;
App::Meta::Url url(link, urlType);
if (urlType == App::Meta::UrlType::repository)
url.branch = std::string(branch);
return url;
}
PyObject *MetadataPy::addUrl(PyObject *args)
{
const char *urlTypeCharStar = nullptr;
const char *link = nullptr;
const char *branch = nullptr;
if (!PyArg_ParseTuple(args, "ss|s", &urlTypeCharStar, &link, &branch))
throw Py::Exception();
getMetadataPtr()->addUrl(urlFromStrings(urlTypeCharStar, link, branch));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeUrl(PyObject *args)
{
const char *urlTypeCharStar = nullptr;
const char *link = nullptr;
const char *branch = nullptr;
if (!PyArg_ParseTuple(args, "ss|s", &urlTypeCharStar, &link, &branch))
throw Py::Exception();
getMetadataPtr()->removeUrl(urlFromStrings(urlTypeCharStar, link, branch));
Py_INCREF(Py_None);
return Py_None;
}
Py::Object dependencyToPyObject(const Meta::Dependency &d)
{
Py::Dict pyDependency;
pyDependency["package"] = Py::String(d.package);
@@ -197,90 +429,338 @@ Py::Object dependencyToPyObject(const Meta::Dependency& d)
pyDependency["version_gt"] = Py::String(d.version_gt);
pyDependency["version_gte"] = Py::String(d.version_gte);
pyDependency["condition"] = Py::String(d.condition);
pyDependency["optional"] = Py::Boolean(d.optional);
switch (d.dependencyType) {
case App::Meta::DependencyType::automatic:
pyDependency["type"] = Py::String ("automatic");
break;
case App::Meta::DependencyType::addon:
pyDependency["type"] = Py::String("addon");
break;
case App::Meta::DependencyType::internal:
pyDependency["type"] = Py::String("internal");
break;
case App::Meta::DependencyType::python:
pyDependency["type"] = Py::String("python");
break;
}
return pyDependency;
}
Meta::Dependency pyObjectToDependency(const Py::Object &d)
{
Py::Dict pyDependency(d);
Meta::Dependency result;
result.package = pyDependency["package"].str();
if (pyDependency.hasKey("version_lt"))
result.version_lt = pyDependency["version_lt"].str();
if (pyDependency.hasKey("version_lte"))
result.version_lte = pyDependency["version_lte"].str();
if (pyDependency.hasKey("version_eq"))
result.version_eq = pyDependency["version_eq"].str();
if (pyDependency.hasKey("version_gt"))
result.version_gt = pyDependency["version_gt"].str();
if (pyDependency.hasKey("version_gte"))
result.version_gte = pyDependency["version_gte"].str();
if (pyDependency.hasKey("condition"))
result.condition = pyDependency["condition"].str();
if (pyDependency.hasKey("optional"))
result.optional = Py::Boolean(pyDependency["optional"]).as_bool();
if (pyDependency.hasKey("type")) {
if (pyDependency["type"].str() == Py::String("automatic"))
result.dependencyType = App::Meta::DependencyType::automatic;
else if (pyDependency["type"].str() == Py::String("internal"))
result.dependencyType = App::Meta::DependencyType::internal;
else if (pyDependency["type"].str() == Py::String("addon"))
result.dependencyType = App::Meta::DependencyType::addon;
else if (pyDependency["type"].str() == Py::String("python"))
result.dependencyType = App::Meta::DependencyType::python;
}
return result;
}
Py::Object MetadataPy::getDepend() const
{
auto dependencies = getMetadataPtr()->depend();
Py::List pyDependencies;
for (const auto& d : dependencies) {
for (const auto &d : dependencies) {
pyDependencies.append(dependencyToPyObject(d));
}
return pyDependencies;
}
void MetadataPy::setDepend(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearDepend();
Py::List deps(list);
for (const auto &dep : deps) {
Py::Dict pyDep(dep);
getMetadataPtr()->addDepend(pyObjectToDependency(pyDep));
}
}
PyObject *MetadataPy::addDepend(PyObject *args)
{
PyObject *dictionary = nullptr;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dictionary))
throw Py::Exception();
Py::Dict pyDep(dictionary);
getMetadataPtr()->addDepend(pyObjectToDependency(pyDep));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeDepend(PyObject *args)
{
PyObject *dictionary = nullptr;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dictionary))
throw Py::Exception();
Py::Dict pyDep(dictionary);
getMetadataPtr()->removeDepend(pyObjectToDependency(pyDep));
Py_INCREF(Py_None);
return Py_None;
}
Py::Object MetadataPy::getConflict() const
{
auto dependencies = getMetadataPtr()->conflict();
Py::List pyDependencies;
for (const auto& d : dependencies) {
for (const auto &d : dependencies) {
pyDependencies.append(dependencyToPyObject(d));
}
return pyDependencies;
}
void MetadataPy::setConflict(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearConflict();
Py::List deps(list);
for (const auto &dep : deps) {
Py::Dict pyDep(dep);
getMetadataPtr()->addConflict(pyObjectToDependency(pyDep));
}
}
PyObject *MetadataPy::addConflict(PyObject *args)
{
PyObject *dictionary = nullptr;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dictionary))
throw Py::Exception();
Py::Dict pyDep(dictionary);
getMetadataPtr()->addConflict(pyObjectToDependency(pyDep));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeConflict(PyObject *args)
{
PyObject *dictionary = nullptr;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dictionary))
throw Py::Exception();
Py::Dict pyDep(dictionary);
getMetadataPtr()->removeConflict(pyObjectToDependency(pyDep));
Py_INCREF(Py_None);
return Py_None;
}
Py::Object MetadataPy::getReplace() const
{
auto dependencies = getMetadataPtr()->replace();
Py::List pyDependencies;
for (const auto& d : dependencies) {
for (const auto &d : dependencies) {
pyDependencies.append(dependencyToPyObject(d));
}
return pyDependencies;
}
void MetadataPy::setReplace(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearReplace();
Py::List deps(list);
for (const auto &dep : deps) {
Py::Dict pyDep(dep);
getMetadataPtr()->addReplace(pyObjectToDependency(pyDep));
}
}
PyObject *MetadataPy::addReplace(PyObject *args)
{
PyObject *dictionary = nullptr;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dictionary))
throw Py::Exception();
Py::Dict pyDep(dictionary);
getMetadataPtr()->addReplace(pyObjectToDependency(pyDep));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeReplace(PyObject *args)
{
PyObject *dictionary = nullptr;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dictionary))
throw Py::Exception();
Py::Dict pyDep(dictionary);
getMetadataPtr()->removeReplace(pyObjectToDependency(pyDep));
Py_INCREF(Py_None);
return Py_None;
}
// Tag, icon, classname, file
Py::Object MetadataPy::getTag() const
{
auto tags = getMetadataPtr()->tag();
Py::List pyTags;
for (const auto& t : tags) {
for (const auto &t : tags) {
pyTags.append(Py::String(t));
}
return pyTags;
}
void MetadataPy::setTag(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearTag();
Py::List tags(list);
for (const auto &tag : tags) {
Py::String pyTag(tag);
getMetadataPtr()->addTag(pyTag.as_std_string());
}
}
PyObject *MetadataPy::addTag(PyObject *args)
{
const char *tag = nullptr;
if (!PyArg_ParseTuple(args, "s", &tag))
throw Py::Exception();
getMetadataPtr()->addTag(tag);
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeTag(PyObject *args)
{
const char *tag = nullptr;
if (!PyArg_ParseTuple(args, "s", &tag))
throw Py::Exception();
getMetadataPtr()->removeTag(tag);
Py_INCREF(Py_None);
return Py_None;
}
Py::Object MetadataPy::getIcon() const
{
return Py::String(getMetadataPtr()->icon().string());
}
void MetadataPy::setIcon(Py::Object args)
{
const char *name;
if (!PyArg_Parse(args.ptr(), "s", &name))
throw Py::Exception();
getMetadataPtr()->setIcon(name);
}
Py::Object MetadataPy::getClassname() const
{
return Py::String(getMetadataPtr()->classname());
}
void MetadataPy::setClassname(Py::Object args)
{
const char *name;
if (!PyArg_Parse(args.ptr(), "s", &name))
throw Py::Exception();
getMetadataPtr()->setClassname(name);
}
Py::Object MetadataPy::getSubdirectory() const
{
return Py::String(getMetadataPtr()->subdirectory().string());
}
void MetadataPy::setSubdirectory(Py::Object args)
{
const char *name;
if (!PyArg_Parse(args.ptr(), "s", &name))
throw Py::Exception();
getMetadataPtr()->setSubdirectory(name);
}
Py::Object MetadataPy::getFile() const
{
auto files = getMetadataPtr()->file();
Py::List pyFiles;
for (const auto& f : files) {
for (const auto &f : files) {
pyFiles.append(Py::String(f.string()));
}
return pyFiles;
}
void MetadataPy::setFile(Py::Object args)
{
PyObject *list = nullptr;
if (!PyArg_Parse(args.ptr(), "O!", &PyList_Type, &list))
throw Py::Exception();
getMetadataPtr()->clearTag();
Py::List files(list);
for (const auto &file : files) {
Py::String pyFile(file);
getMetadataPtr()->addFile(pyFile.as_std_string());
}
}
PyObject *MetadataPy::addFile(PyObject *args)
{
const char *file = nullptr;
if (!PyArg_ParseTuple(args, "s", &file))
throw Py::Exception();
getMetadataPtr()->addFile(file);
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeFile(PyObject *args)
{
const char *file = nullptr;
if (!PyArg_ParseTuple(args, "s", &file))
throw Py::Exception();
getMetadataPtr()->removeFile(file);
Py_INCREF(Py_None);
return Py_None;
}
Py::Object MetadataPy::getContent() const
{
auto content = getMetadataPtr()->content();
std::set<std::string> keys;
for (const auto& item : content) {
for (const auto &item : content) {
keys.insert(item.first);
}
// For the Python, we'll use a dictionary of lists to store the content components:
Py::Dict pyContent;
for (const auto& key : keys) {
for (const auto &key : keys) {
Py::List pyContentForKey;
auto elements = content.equal_range(key);
for (auto & element = elements.first; element != elements.second; ++element) {
for (auto &element = elements.first; element != elements.second; ++element) {
auto contentMetadataItem = new MetadataPy(new Metadata(element->second));
pyContentForKey.append(Py::asObject(contentMetadataItem));
}
@@ -289,19 +769,38 @@ Py::Object MetadataPy::getContent() const
return pyContent;
}
PyObject* MetadataPy::getGenericMetadata(PyObject* args)
void MetadataPy::setContent(Py::Object arg)
{
const char* name;
PyObject *obj = nullptr;
if (!PyArg_Parse(arg.ptr(), "O!", &PyList_Type, &obj))
throw Py::Exception();
getMetadataPtr()->clearContent();
Py::Dict outerDict(obj);
for (const auto &pyContentType : outerDict) {
auto contentType = Py::String(pyContentType.first).as_std_string();
auto contentList = Py::List(pyContentType.second);
for (const auto& contentItem : contentList) {
auto item = static_cast<MetadataPy *>(contentItem.ptr());
getMetadataPtr()->addContentItem(contentType, *(item->getMetadataPtr()));
}
}
}
PyObject *MetadataPy::getGenericMetadata(PyObject *args)
{
const char *name;
if (!PyArg_ParseTuple(args, "s", &name))
return nullptr;
auto gm = (*getMetadataPtr())[name];
Py::List pyGenericMetadata;
for (const auto& item : gm) {
for (const auto &item : gm) {
Py::Dict pyItem;
pyItem["contents"] = Py::String(item.contents);
Py::Dict pyAttributes;
for (const auto& attribute : item.attributes) {
for (const auto &attribute : item.attributes) {
pyAttributes[attribute.first] = Py::String(attribute.second);
}
pyItem["attributes"] = pyAttributes;
@@ -317,12 +816,14 @@ Py::Object MetadataPy::getFreeCADMin() const
void MetadataPy::setFreeCADMin(Py::Object args)
{
char* version = nullptr;
PyObject* p = args.ptr();
if (!PyArg_ParseTuple(p, "s", &version))
return;
getMetadataPtr()->setFreeCADMin(App::Meta::Version(version));
char *version = nullptr;
PyObject *p = args.ptr();
if (!PyArg_Parse(p, "z", &version))
throw Py::Exception();
if (version)
getMetadataPtr()->setFreeCADMin(App::Meta::Version(version));
else
getMetadataPtr()->setFreeCADMin(App::Meta::Version());
}
Py::Object MetadataPy::getFreeCADMax() const
@@ -332,15 +833,18 @@ Py::Object MetadataPy::getFreeCADMax() const
void MetadataPy::setFreeCADMax(Py::Object args)
{
char* version = nullptr;
PyObject* p = args.ptr();
if (!PyArg_ParseTuple(p, "s", &version))
return;
char *version = nullptr;
PyObject *p = args.ptr();
if (!PyArg_Parse(p, "z", &version))
throw Py::Exception();
getMetadataPtr()->setFreeCADMax(App::Meta::Version(version));
if (version)
getMetadataPtr()->setFreeCADMax(App::Meta::Version(version));
else
getMetadataPtr()->setFreeCADMax(App::Meta::Version());
}
PyObject* MetadataPy::getFirstSupportedFreeCADVersion(PyObject* p)
PyObject *MetadataPy::getFirstSupportedFreeCADVersion(PyObject *p)
{
if (!PyArg_ParseTuple(p, ""))
return nullptr;
@@ -351,7 +855,7 @@ PyObject* MetadataPy::getFirstSupportedFreeCADVersion(PyObject* p)
auto content = getMetadataPtr()->content();
auto result = App::Meta::Version();
for (const auto& item : content) {
for (const auto &item : content) {
auto minVersion = item.second.freecadmin();
if (minVersion != App::Meta::Version())
if (result == App::Meta::Version() || minVersion < result)
@@ -366,7 +870,7 @@ PyObject* MetadataPy::getFirstSupportedFreeCADVersion(PyObject* p)
}
}
PyObject* MetadataPy::getLastSupportedFreeCADVersion(PyObject* p)
PyObject *MetadataPy::getLastSupportedFreeCADVersion(PyObject *p)
{
if (!PyArg_ParseTuple(p, ""))
return nullptr;
@@ -377,7 +881,7 @@ PyObject* MetadataPy::getLastSupportedFreeCADVersion(PyObject* p)
auto content = getMetadataPtr()->content();
auto result = App::Meta::Version();
for (const auto& item : content) {
for (const auto &item : content) {
auto maxVersion = item.second.freecadmax();
if (maxVersion != App::Meta::Version())
if (result == App::Meta::Version() || maxVersion > result)
@@ -392,7 +896,7 @@ PyObject* MetadataPy::getLastSupportedFreeCADVersion(PyObject* p)
}
}
PyObject* MetadataPy::supportsCurrentFreeCAD(PyObject* p)
PyObject *MetadataPy::supportsCurrentFreeCAD(PyObject *p)
{
if (!PyArg_ParseTuple(p, ""))
return nullptr;
@@ -401,12 +905,41 @@ PyObject* MetadataPy::supportsCurrentFreeCAD(PyObject* p)
return Py::new_reference_to(Py::Boolean(result));
}
PyObject* MetadataPy::getCustomAttributes(const char* /*attr*/) const
PyObject* MetadataPy::addContentItem(PyObject* arg)
{
char *contentType = nullptr;
PyObject *contentItem = nullptr;
if (!PyArg_ParseTuple(arg, "sO!", &contentType, &(App::MetadataPy::Type), &contentItem))
return nullptr;
if (!contentItem || !contentType)
return nullptr;
auto item = static_cast<MetadataPy *>(contentItem);
getMetadataPtr()->addContentItem(contentType, *(item->getMetadataPtr()));
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::removeContentItem(PyObject *arg)
{
char *contentType = nullptr;
char *contentName = nullptr;
if (!PyArg_ParseTuple(arg, "ss", &contentType, &contentName))
return nullptr;
if (contentType && contentName)
getMetadataPtr()->removeContentItem(contentType, contentName);
Py_INCREF(Py_None);
return Py_None;
}
PyObject *MetadataPy::getCustomAttributes(const char * /*attr*/) const
{
return nullptr;
}
int MetadataPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
int MetadataPy::setCustomAttributes(const char * /*attr*/, PyObject * /*obj*/)
{
return 0;
}

View File

@@ -9,9 +9,11 @@ SET(AddonManager_SRCS
AddonManager.ui
addonmanager_devmode.py
addonmanager_devmode_add_content.py
addonmanager_devmode_license_selector.py
addonmanager_devmode_dependencies.py
addonmanager_devmode_license_selector.py
addonmanager_devmode_person_editor.py
addonmanager_devmode_predictor.py
addonmanager_devmode_validators.py
addonmanager_git.py
addonmanager_macro.py
addonmanager_utilities.py

View File

@@ -27,24 +27,25 @@ import os
import FreeCAD
import FreeCADGui
from PySide2.QtWidgets import QFileDialog, QTableWidgetItem
from PySide2.QtWidgets import QFileDialog, QTableWidgetItem, QListWidgetItem, QDialog
from PySide2.QtGui import (
QIcon,
QValidator,
QRegularExpressionValidator,
QPixmap,
)
from PySide2.QtCore import QRegularExpression, Qt
from PySide2.QtCore import Qt
from addonmanager_git import GitManager
from addonmanager_devmode_license_selector import LicenseSelector
from addonmanager_devmode_person_editor import PersonEditor
from addonmanager_devmode_add_content import AddContent
from addonmanager_devmode_validators import NameValidator, VersionValidator
translate = FreeCAD.Qt.translate
# pylint: disable=too-few-public-methods
ContentTypeRole = Qt.UserRole
ContentIndexRole = Qt.UserRole + 1
class AddonGitInterface:
"""Wrapper to handle the git calls needed by this class"""
@@ -66,94 +67,20 @@ class AddonGitInterface:
return AddonGitInterface.git_manager.get_branches(self.path)
return []
@property
def committers(self):
"""The commiters to this repo, in the last ten commits"""
if self.git_exists:
return AddonGitInterface.git_manager.get_last_committers(self.path, 10)
return []
class NameValidator(QValidator):
"""Simple validator to exclude characters that are not valid in filenames."""
@property
def authors(self):
"""The commiters to this repo, in the last ten commits"""
if self.git_exists:
return AddonGitInterface.git_manager.get_last_authors(self.path, 10)
return []
invalid = '/\\?%*:|"<>'
def validate(self, value: str, _: int):
"""Check the value against the validator"""
for char in value:
if char in NameValidator.invalid:
return QValidator.Invalid
return QValidator.Acceptable
def fixup(self, value: str) -> str:
"""Remove invalid characters from value"""
result = ""
for char in value:
if char not in NameValidator.invalid:
result += char
return result
class SemVerValidator(QRegularExpressionValidator):
"""Implements the officially-recommended regex validator for Semantic version numbers."""
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
semver_re = QRegularExpression(
r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
+ r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
+ r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
+ r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
)
def __init__(self):
super().__init__()
self.setRegularExpression(SemVerValidator.semver_re)
@classmethod
def check(cls, value: str) -> bool:
"""Returns true if value validates, and false if not"""
return cls.semver_re.match(value).hasMatch()
class CalVerValidator(QRegularExpressionValidator):
"""Implements a basic regular expression validator that makes sure an entry corresponds
to a CalVer version numbering standard."""
calver_re = QRegularExpression(
r"^(?P<major>[1-9]\d{3})\.(?P<minor>[0-9]{1,2})\.(?P<patch>0|[0-9]{0,2})"
+ r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
+ r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
+ r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
)
def __init__(self):
super().__init__()
self.setRegularExpression(CalVerValidator.calver_re)
@classmethod
def check(cls, value: str) -> bool:
"""Returns true if value validates, and false if not"""
return cls.calver_re.match(value).hasMatch()
class VersionValidator(QValidator):
"""Implements the officially-recommended regex validator for Semantic version numbers, and a
decent approximation of the same thing for CalVer-style version numbers."""
def __init__(self):
super().__init__()
self.semver = SemVerValidator()
self.calver = CalVerValidator()
def validate(self, value: str, position: int):
"""Called for validation, returns a tuple of the validation state, the value, and the
position."""
semver_result = self.semver.validate(value, position)
calver_result = self.calver.validate(value, position)
if semver_result[0] == QValidator.Acceptable:
return semver_result
if calver_result[0] == QValidator.Acceptable:
return calver_result
if semver_result[0] == QValidator.Intermediate:
return semver_result
if calver_result[0] == QValidator.Intermediate:
return calver_result
return (QValidator.Invalid, value, position)
class DeveloperMode:
@@ -174,6 +101,8 @@ class DeveloperMode:
self.current_mod: str = ""
self.git_interface = None
self.has_toplevel_icon = False
self.metadata = None
self._setup_dialog_signals()
self.dialog.displayNameLineEdit.setValidator(NameValidator())
@@ -202,7 +131,9 @@ class DeveloperMode:
"""Show the main dev mode dialog"""
if parent:
self.dialog.setParent(parent)
self.dialog.exec()
result = self.dialog.exec()
if result == QDialog.Accepted:
self._sync_metadata_to_ui()
def _populate_dialog(self, path_to_repo):
"""Populate this dialog using the best available parsing of the contents of the repo at
@@ -215,7 +146,6 @@ class DeveloperMode:
self._scan_for_git_info(self.current_mod)
metadata_path = os.path.join(path_to_repo, "package.xml")
metadata = None
if os.path.exists(metadata_path):
try:
self.metadata = FreeCAD.Metadata(metadata_path)
@@ -239,19 +169,21 @@ class DeveloperMode:
+ "\n\n"
)
if self.metadata:
self.dialog.displayNameLineEdit.setText(self.metadata.Name)
self.dialog.descriptionTextEdit.setPlainText(self.metadata.Description)
self.dialog.versionLineEdit.setText(self.metadata.Version)
self._clear_all_fields()
self._populate_people_from_metadata(self.metadata)
self._populate_licenses_from_metadata(self.metadata)
self._populate_urls_from_metadata(self.metadata)
self._populate_contents_from_metadata(self.metadata)
if not self.metadata:
self._predict_metadata()
self._populate_icon_from_metadata(self.metadata)
else:
self._populate_without_metadata()
self.dialog.displayNameLineEdit.setText(self.metadata.Name)
self.dialog.descriptionTextEdit.setPlainText(self.metadata.Description)
self.dialog.versionLineEdit.setText(self.metadata.Version)
self._populate_people_from_metadata(self.metadata)
self._populate_licenses_from_metadata(self.metadata)
self._populate_urls_from_metadata(self.metadata)
self._populate_contents_from_metadata(self.metadata)
self._populate_icon_from_metadata(self.metadata)
def _populate_people_from_metadata(self, metadata):
"""Use the passed metadata object to populate the maintainers and authors"""
@@ -305,6 +237,7 @@ class DeveloperMode:
)
def _add_license_row(self, row: int, name: str, path: str):
""" Add a row to the table of licenses """
self.dialog.licensesTableWidget.insertRow(row)
self.dialog.licensesTableWidget.setItem(row, 0, QTableWidgetItem(name))
self.dialog.licensesTableWidget.setItem(row, 1, QTableWidgetItem(path))
@@ -349,6 +282,7 @@ class DeveloperMode:
contents = metadata.Content
self.dialog.contentsListWidget.clear()
for content_type in contents:
counter = 0
for item in contents[content_type]:
contents_string = f"[{content_type}] "
info = []
@@ -378,7 +312,11 @@ class DeveloperMode:
)
contents_string += ", ".join(info)
self.dialog.contentsListWidget.addItem(contents_string)
item = QListWidgetItem (contents_string)
item.setData(ContentTypeRole, content_type)
item.setData(ContentIndexRole, counter)
self.dialog.contentsListWidget.addItem(item)
counter += 1
def _populate_icon_from_metadata(self, metadata):
"""Use the passed metadata object to populate the icon fields"""
@@ -407,9 +345,16 @@ class DeveloperMode:
self.dialog.iconDisplayLabel.setPixmap(icon_data.pixmap(32, 32))
self.dialog.iconPathLineEdit.setText(icon)
def _populate_without_metadata(self):
def _predict_metadata(self):
"""If there is no metadata, try to guess at values for it"""
self._clear_all_fields()
self.metadata = FreeCAD.Metadata()
self._predict_author_info()
self._predict_name()
self._predict_description()
self._predict_contents()
self._predict_icon()
self._predict_urls()
self._predict_license()
def _scan_for_git_info(self, path):
"""Look for branch availability"""
@@ -436,6 +381,57 @@ class DeveloperMode:
self.dialog.licensesTableWidget.setRowCount(0)
self.dialog.peopleTableWidget.setRowCount(0)
def _predict_author_info(self):
""" Look at the git commit history and attempt to discern maintainer and author
information."""
self.git_interface = AddonGitInterface(path)
if self.git_interface.git_exists:
committers = self.git_interface.get_last_committers()
else:
return
# This is a dictionary keyed to the author's name (which can be many different
# things, depending on the author) containing two fields, "email" and "count". It
# is common for there to be multiple entries representing the same human being,
# so a passing attempt is made to reconcile:
for key in committers:
emails = committers[key]["email"]
if "GitHub" in key:
# Robotic merge commit (or other similar), ignore
continue
# Does any other committer share any of these emails?
for other_key in committers:
if other_key == key:
continue
other_emails = committers[other_key]["email"]
for other_email in other_emails:
if other_email in emails:
# There is overlap in the two email lists, so this is probably the
# same author, with a different name (username, pseudonym, etc.)
if not committers[key]["aka"]:
committers[key]["aka"] = set()
committers[key]["aka"].add(other_key)
committers[key]["count"] += committers[other_key]["count"]
committers[key]["email"].combine(committers[other_key]["email"])
committers.remove(other_key)
break
maintainers = []
for name,info in committers.items():
if info["aka"]:
for other_name in info["aka"]:
# Heuristic: the longer name is more likely to be the actual legal name
if len(other_name) > len(name):
name = other_name
# There is no logical basis to choose one email address over another, so just
# take the first one
email = info["email"][0]
commit_count = info["count"]
maintainers.append( {"name":name,"email":email,"count":commit_count} )
# Sort by count of commits
maintainers.sort(lambda i:i["count"],reverse=True)
def _setup_dialog_signals(self):
"""Set up the signal and slot connections for the main dialog."""
@@ -467,14 +463,54 @@ class DeveloperMode:
self.dialog.contentsListWidget.itemSelectionChanged.connect(self._content_selection_changed)
self.dialog.contentsListWidget.itemDoubleClicked.connect(self._edit_content)
# Finally, populate the combo boxes, etc.
self._populate_combo()
if self.dialog.pathToAddonComboBox.currentIndex() != -1:
self._populate_dialog(self.dialog.pathToAddonComboBox.currentText())
# Disable all of the "Remove" buttons until something is selected
self.dialog.removeLicenseToolButton.setDisabled(True)
self.dialog.removePersonToolButton.setDisabled(True)
self.dialog.removeContentItemToolButton.setDisabled(True)
def _sync_metadata_to_ui(self):
""" Take the data from the UI fields and put it into the stored metadata
object. Only overwrites known data fields: unknown metadata will be retained. """
self.metadata.Name = self.dialog.displayNameLineEdit.text()
self.metadata.Description = self.descriptionTextEdit.text()
self.metadata.Version = self.dialog.versionLineEdit.text()
self.metadata.Icon = self.dialog.iconPathLineEdit.text()
url = {}
url["website"] = self.dialog.websiteURLLineEdit.text()
url["repository"] = self.dialog.repositoryURLLineEdit.text()
url["bugtracker"] = self.dialog.bugtrackerURLLineEdit.text()
url["readme"] = self.dialog.readmeURLLineEdit.text()
url["documentation"] = self.dialog.documentationURLLineEdit.text()
self.metadata.setUrl(url)
# Licenses:
licenses = []
for row in range(self.dialog.licensesTableWidget.rowCount()):
license = {}
license["name"] = self.dialog.licensesTableWidget.item(row,0).text()
license["file"] = self.dialog.licensesTableWidget.item(row,1).text()
licenses.append(license)
self.metadata.setLicense(licenses)
# Maintainers:
maintainers = []
authors = []
for row in range(self.dialog.peopleTableWidget.rowCount()):
person = {}
person["name"] = self.dialog.peopleTableWidget.item(row,1).text()
person["email"] = self.dialog.peopleTableWidget.item(row,2).text()
if self.dialog.peopleTableWidget.item(row,0).data(Qt.UserRole) == "maintainer":
maintainers.append(person)
elif self.dialog.peopleTableWidget.item(row,0).data(Qt.UserRole) == "author":
authors.append(person)
# Content:
###############################################################################################
# DIALOG SLOTS
@@ -553,6 +589,7 @@ class DeveloperMode:
recent_mods_group.SetString(entry_name, mod)
def _person_selection_changed(self):
""" Callback: the current selection in the peopleTableWidget changed """
items = self.dialog.peopleTableWidget.selectedItems()
if items:
self.dialog.removePersonToolButton.setDisabled(False)
@@ -560,6 +597,7 @@ class DeveloperMode:
self.dialog.removePersonToolButton.setDisabled(True)
def _license_selection_changed(self):
""" Callback: the current selection in the licensesTableWidget changed """
items = self.dialog.licensesTableWidget.selectedItems()
if items:
self.dialog.removeLicenseToolButton.setDisabled(False)
@@ -567,6 +605,7 @@ class DeveloperMode:
self.dialog.removeLicenseToolButton.setDisabled(True)
def _add_license_clicked(self):
""" Callback: The Add License button was clicked """
license_selector = LicenseSelector(self.current_mod)
short_code, path = license_selector.exec()
if short_code:
@@ -575,6 +614,7 @@ class DeveloperMode:
)
def _remove_license_clicked(self):
""" Callback: the Remove License button was clicked """
items = self.dialog.licensesTableWidget.selectedIndexes()
if items:
# We only support single-selection, so can just pull the row # from
@@ -582,6 +622,7 @@ class DeveloperMode:
self.dialog.licensesTableWidget.removeRow(items[0].row())
def _edit_license(self, item):
""" Callback: a license row was double-clicked """
row = item.row()
short_code = self.dialog.licensesTableWidget.item(row, 0).text()
path = self.dialog.licensesTableWidget.item(row, 1).text()
@@ -592,12 +633,14 @@ class DeveloperMode:
self._add_license_row(row, short_code, path)
def _add_person_clicked(self):
""" Callback: the Add Person button was clicked """
dlg = PersonEditor()
person_type, name, email = dlg.exec()
if person_type and name:
self._add_person_row(row, person_type, name, email)
def _remove_person_clicked(self):
""" Callback: the Remove Person button was clicked """
items = self.dialog.peopleTableWidget.selectedIndexes()
if items:
# We only support single-selection, so can just pull the row # from
@@ -605,6 +648,7 @@ class DeveloperMode:
self.dialog.peopleTableWidget.removeRow(items[0].row())
def _edit_person(self, item):
""" Callback: a row in the peopleTableWidget was double-clicked """
row = item.row()
person_type = self.dialog.peopleTableWidget.item(row, 0).data(Qt.UserRole)
name = self.dialog.peopleTableWidget.item(row, 1).text()
@@ -621,14 +665,51 @@ class DeveloperMode:
def _add_content_clicked(self):
""" Callback: The Add Content button was clicked """
dlg = AddContent(self.current_mod, self.metadata)
dlg.exec()
singleton = False
if self.dialog.contentsListWidget.count() == 0:
singleton = True
content_type,new_metadata = dlg.exec(singleton=singleton)
if content_type and new_metadata:
self.metadata.addContentItem(content_type, new_metadata)
self._populate_contents_from_metadata(self.metadata)
def _remove_content_clicked(self):
pass
""" Callback: the remove content button was clicked """
item = self.dialog.contentsListWidget.currentItem()
if not item:
return
content_type = item.data(ContentTypeRole)
content_index = item.data(ContentIndexRole)
if self.metadata.Content[content_type] and content_index < len(self.metadata.Content[content_type]):
content_name = self.metadata.Content[content_type][content_index].Name
self.metadata.removeContentItem(content_type,content_name)
self._populate_contents_from_metadata(self.metadata)
def _content_selection_changed(self):
pass
""" Callback: the selected content item changed """
items = self.dialog.contentsListWidget.selectedItems()
if items:
self.dialog.removeContentItemToolButton.setDisabled(False)
else:
self.dialog.removeContentItemToolButton.setDisabled(True)
def _edit_content(self, item):
pass
""" Callback: a content row was double-clicked """
dlg = AddContent(self.current_mod, self.metadata)
content_type = item.data(ContentTypeRole)
content_index = item.data(ContentIndexRole)
content = self.metadata.Content
metadata = content[content_type][content_index]
old_name = metadata.Name
new_type, new_metadata = dlg.exec(content_type, metadata, len(content) == 1)
if new_type and new_metadata:
self.metadata.removeContentItem(content_type, old_name)
self.metadata.addContentItem(new_type, new_metadata)
self._populate_contents_from_metadata(self.metadata)

View File

@@ -35,6 +35,8 @@ from PySide2.QtWidgets import QDialog, QLayout, QFileDialog, QTableWidgetItem
from PySide2.QtGui import QIcon
from PySide2.QtCore import Qt
from addonmanager_devmode_validators import VersionValidator, NameValidator, PythonIdentifierValidator
# pylint: disable=too-few-public-methods
translate = FreeCAD.Qt.translate
@@ -77,6 +79,24 @@ class AddContent:
self._freecad_versions_clicked
)
self.dialog.versionLineEdit.setValidator(VersionValidator())
self.dialog.prefPackNameLineEdit.setValidator(NameValidator())
self.dialog.displayNameLineEdit.setValidator(NameValidator())
self.dialog.workbenchClassnameLineEdit.setValidator(PythonIdentifierValidator())
self.dialog.addPersonToolButton.setIcon(
QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
)
self.dialog.removePersonToolButton.setIcon(
QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
)
self.dialog.addLicenseToolButton.setIcon(
QIcon.fromTheme("add", QIcon(":/icons/list-add.svg"))
)
self.dialog.removeLicenseToolButton.setIcon(
QIcon.fromTheme("remove", QIcon(":/icons/list-remove.svg"))
)
def exec(
self,
content_kind: str = "workbench",
@@ -87,7 +107,8 @@ class AddContent:
is accepted, or None if it is rejected. This metadata object represents a single
new content item. It's returned as a tuple with the object type as the first component,
and the metadata object itself as the second."""
self.metadata = metadata
if metadata:
self.metadata = FreeCAD.Metadata(metadata) # Deep copy
self.dialog.singletonCheckBox.setChecked(singleton)
if singleton:
# This doesn't happen automatically the first time
@@ -104,12 +125,15 @@ class AddContent:
self.dialog.addonKindComboBox.setCurrentIndex(index)
if metadata:
self._populate_dialog(metadata)
self.dialog.removeLicenseToolButton.setDisabled(True)
self.dialog.removePersonToolButton.setDisabled(True)
self.dialog.layout().setSizeConstraint(QLayout.SetFixedSize)
result = self.dialog.exec()
if result == QDialog.Accepted:
return self._generate_metadata()
return None
return None, None
def _populate_dialog(self, metadata: FreeCAD.Metadata) -> None:
"""Fill in the dialog with the details from the passed metadata object"""
@@ -117,9 +141,11 @@ class AddContent:
if addon_kind == "workbench":
self.dialog.workbenchClassnameLineEdit.setText(metadata.Classname)
elif addon_kind == "macro":
pass
files = self.metadata.File
if files:
self.dialog.macroFileLineEdit.setText(files[0])
elif addon_kind == "preferencepack":
pass
self.dialog.prefPackNameLineEdit.setText(self.metadata.Name)
else:
raise RuntimeError("Invalid data found for selection")
@@ -140,6 +166,12 @@ class AddContent:
else:
self.dialog.subdirectoryLineEdit.setText("")
self.dialog.displayNameLineEdit.setText(metadata.Name)
self.dialog.descriptionTextEdit.setPlainText(metadata.Description)
self.dialog.versionLineEdit.setText(metadata.Version)
#TODO: Add people and licenses
def _set_icon(self, icon_relative_path):
"""Load the icon and display it, and its path, in the dialog."""
icon_path = os.path.join(
@@ -161,7 +193,56 @@ class AddContent:
def _generate_metadata(self) -> Tuple[str, FreeCAD.Metadata]:
"""Create and return a new metadata object based on the contents of the dialog."""
return ("workbench", FreeCAD.Metadata())
if not self.metadata:
self.metadata = FreeCAD.Metadata()
##########################################################################################
# Required data:
current_data:str = self.dialog.addonKindComboBox.currentData()
if current_data == "preferencepack":
self.metadata.Name = self.dialog.prefPackNameLineEdit.text()
elif self.dialog.displayNameLineEdit.text():
self.metadata.Name = self.dialog.displayNameLineEdit.text()
if current_data == "workbench":
self.metadata.Classname = self.dialog.workbenchClassnameLineEdit.text()
elif current_data == "macro":
self.metadata.File = [self.dialog.macroFileLineEdit.text()]
##########################################################################################
# Early return if this is the only addon
if self.dialog.singletonCheckBox.isChecked():
return (current_data, self.metadata)
self.metadata.Icon = self.dialog.iconLineEdit.text()
# Otherwise, process the rest of the metadata (display name is already done)
self.metadata.Description = self.dialog.descriptionTextEdit.document().toPlainText()
self.metadata.Version = self.dialog.versionLineEdit.text()
maintainers = []
authors = []
for row in range(self.dialog.peopleTableWidget.rowCount()):
person_type = self.dialog.peopleTableWidget.item(row, 0).data()
name = self.dialog.peopleTableWidget.item(row, 1).text()
email = self.dialog.peopleTableWidget.item(row, 2).text()
if person_type == "maintainer":
maintainers.append({"name":name,"email":email})
elif person_type == "author":
authors.append({"name":name,"email":email})
self.metadata.Maintainer = maintainers
self.metadata.Author = authors
licenses = []
for row in range(self.dialog.licensesTableWidget.rowCount()):
license = {}
license["name"] = self.dialog.licensesTableWidget.item(row, 0).text
license["file"] = self.dialog.licensesTableWidget.item(row, 1).text()
licenses.append(license)
self.metadata.License = licenses
return (self.dialog.addonKindComboBox.currentData(), self.metadata)
###############################################################################################
# DIALOG SLOTS
@@ -180,6 +261,9 @@ class AddContent:
dir=start_dir,
)
if not new_icon_path:
return
base_path = self.path_to_addon.replace("/", os.path.sep)
icon_path = new_icon_path.replace("/", os.path.sep)
@@ -192,6 +276,7 @@ class AddContent:
)
return
self._set_icon(new_icon_path[len(base_path) :])
self.metadata.Icon = new_icon_path[len(base_path) :]
def _browse_for_subdirectory_clicked(self):
"""Callback: when the "Browse..." button for the subdirectory field is clicked"""
@@ -205,6 +290,8 @@ class AddContent:
),
dir=start_dir,
)
if not new_subdir_path:
return
if new_subdir_path[-1] != "/":
new_subdir_path += "/"
@@ -238,20 +325,27 @@ class AddContent:
def _tags_clicked(self):
"""Show the tag editor"""
tags = []
if not self.metadata:
self.metadata = FreeCAD.Metadata()
if self.metadata:
tags = self.metadata.Tag
dlg = EditTags(tags)
new_tags = dlg.exec()
self.metadata.Tag = new_tags
def _freecad_versions_clicked(self):
"""Show the FreeCAD version editor"""
if not self.metadata:
self.metadata = FreeCAD.Metadata()
dlg = EditFreeCADVersions()
dlg.exec()
dlg.exec(self.metadata)
def _dependencies_clicked(self):
"""Show the dependencies editor"""
if not self.metadata:
self.metadata = FreeCAD.Metadata()
dlg = EditDependencies()
dlg.exec()
result = dlg.exec(self.metadata)
class EditTags:
@@ -304,20 +398,34 @@ class EditDependencies:
self.dialog.removeDependencyToolButton.setDisabled(True)
def exec(self):
def exec(self, metadata:FreeCAD.Metadata):
"""Execute the dialog"""
self.dialog.exec()
self.metadata = FreeCAD.Metadata(metadata) # Make a copy, in case we cancel
row = 0
for dep in self.metadata.Depend:
dep_type = dep["type"]
dep_name = dep["package"]
dep_optional = dep["optional"]
self._add_row(row, dep_type, dep_name, dep_optional)
row += 1
result = self.dialog.exec()
if result == QDialog.Accepted:
metadata.Depend = self.metadata.Depend
def _add_dependency_clicked(self):
"""Callback: The add button was clicked"""
dlg = EditDependency()
dep_type, dep_name, dep_optional = dlg.exec()
row = self.dialog.tableWidget.rowCount()
self._add_row(row, dep_type, dep_name, dep_optional)
if dep_name:
row = self.dialog.tableWidget.rowCount()
self._add_row(row, dep_type, dep_name, dep_optional)
self.metadata.addDepend({"package":dep_name, "type":dep_type, "optional":dep_optional})
def _add_row(self, row, dep_type, dep_name, dep_optional):
"""Utility function to add a row to the table."""
translations = {
"automatic": translate("AddonsInstaller", "Automatic"),
"workbench": translate("AddonsInstaller", "Workbench"),
"addon": translate("AddonsInstaller", "Addon"),
"python": translate("AddonsInstaller", "Python"),
@@ -338,6 +446,10 @@ class EditDependencies:
items = self.dialog.tableWidget.selectedItems()
if items:
row = items[0].row()
dep_type = self.dialog.tableWidget.item(row, 0).data(Qt.UserRole)
dep_name = self.dialog.tableWidget.item(row, 1).text()
dep_optional = bool(self.dialog.tableWidget.item(row, 2))
self.metadata.removeDepend({"package":dep_name, "type":dep_type, "optional":dep_optional})
self.dialog.tableWidget.removeRow(row)
def _edit_dependency(self, item):
@@ -347,8 +459,10 @@ class EditDependencies:
dep_type = self.dialog.tableWidget.item(row, 0).data(Qt.UserRole)
dep_name = self.dialog.tableWidget.item(row, 1).text()
dep_optional = bool(self.dialog.tableWidget.item(row, 2))
dep_type, dep_name, dep_optional = dlg.exec(dep_type, dep_name, dep_optional)
new_dep_type, new_dep_name, new_dep_optional = dlg.exec(dep_type, dep_name, dep_optional)
if dep_type and dep_name:
self.metadata.removeDepend({"package":dep_name, "type":dep_type, "optional":dep_optional})
self.metadata.addDepend({"package":new_dep_name, "type":new_dep_type, "optional":new_dep_optional})
self.dialog.tableWidget.removeRow(row)
self._add_row(row, dep_type, dep_name, dep_optional)
@@ -384,9 +498,8 @@ class EditDependency:
self._dependency_selection_changed
)
self.dialog.typeComboBox.setCurrentIndex(
2
) # Expect mostly Python dependencies...
# Expect mostly Python dependencies...
self.dialog.typeComboBox.setCurrentIndex(2)
def exec(
self, dep_type="", dep_name="", dep_optional=False
@@ -488,9 +601,22 @@ class EditFreeCADVersions:
)
)
def exec(self):
def exec(self, metadata:FreeCAD.Metadata):
"""Execute the dialog"""
self.dialog.exec()
if metadata.FreeCADMin != "0.0.0":
self.dialog.minVersionLineEdit.setText(metadata.FreeCADMin)
if metadata.FreeCADMax != "0.0.0":
self.dialog.maxVersionLineEdit.setText(metadata.FreeCADMax)
result = self.dialog.exec()
if result == QDialog.Accepted:
if self.dialog.minVersionLineEdit.text():
metadata.FreeCADMin = self.dialog.minVersionLineEdit.text()
else:
metadata.FreeCADMin = None
if self.dialog.maxVersionLineEdit.text():
metadata.FreeCADMax = self.dialog.maxVersionLineEdit.text()
else:
metadata.FreeCADMax = None
class EditAdvancedVersions:

View File

@@ -60,7 +60,7 @@ class LicenseSelector:
"https://creativecommons.org/choose/zero/",
),
"GPLv2": (
"GNU Lesser General Public License version 2",
"GNU General Public License version 2",
"https://opensource.org/licenses/GPL-2.0",
),
"GPLv3": (

View File

@@ -0,0 +1,154 @@
# ***************************************************************************
# * *
# * Copyright (c) 2022 FreeCAD Project Association *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD is free software: you can redistribute it and/or modify it *
# * under the terms of the GNU Lesser General Public License as *
# * published by the Free Software Foundation, either version 2.1 of the *
# * License, or (at your option) any later version. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, but *
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
# * Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Lesser General Public *
# * License along with FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
""" Class to guess metadata based on folder contents. Note that one of the functions
of this file is to guess the license being applied to the new software package based
in its contents. It is up to the user to make the final determination about whether
the selected license is the correct one, and inclusion here shouldn't be construed as
endorsement of any particular license. In addition, the inclusion of those text strings
does not imply a modification to the license for THIS software, which is licensed
under the LGPLv2.1 license (as stated above)."""
import os
import FreeCAD
from addonmanager_git import initialize_git, GitFailed
class Predictor:
""" Guess the appropriate metadata to apply to a project based on various parameters
found in the supplied directory. """
def __init__(self):
self.path = None
self.metadata = FreeCAD.Metadata()
self.license_data = None
self.license_file = ""
def predict_metadata(self, path:os.PathLike) -> FreeCAD.Metadata:
""" Create a predicted Metadata object based on the contents of the passed-in directory """
self.path = path
self._predict_author_info()
self._predict_name()
self._predict_description()
self._predict_contents()
self._predict_icon()
self._predict_urls()
self._predict_license()
def _predict_author_info(self):
""" Predict the author and maintainer info based on git history """
def _predict_name(self):
""" Predict the name based on the local path name and/or the contents of a
README.md file. """
def _predict_description(self):
""" Predict the description based on the contents of a README.md file. """
def _predict_contents(self):
""" Predict the contents based on the contents of the directory. """
def _predict_icon(self):
""" Predict the icon based on either a class which defines an Icon member, or
the contents of the local directory structure. """
def _predict_urls(self):
""" Predict the URLs based on git settings """
def _predict_license(self):
""" Predict the license based on any existing license file. """
# These are processed in order, so the BSD 3 clause must come before the 2, for example, because
# the only difference between them is the additional clause.
known_strings = {
"Apache-2.0": (
"Apache License, Version 2.0",
"Apache License\nVersion 2.0, January 2004",
),
"BSD-3-Clause": (
"The 3-Clause BSD License",
"3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission."
),
"BSD-2-Clause": (
"The 2-Clause BSD License",
"2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution."
),
"CC0v1": (
"CC0 1.0 Universal",
"voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms"
),
"GPLv2": (
"GNU General Public License version 2",
"GNU GENERAL PUBLIC LICENSE\nVersion 2, June 1991"
),
"GPLv3": (
"GNU General Public License version 3",
"The GNU General Public License is a free, copyleft license for software and other kinds of works."
),
"LGPLv2.1": (
"GNU Lesser General Public License version 2.1",
"GNU Lesser General Public License\nVersion 2.1, February 1999"
),
"LGPLv3": (
"GNU Lesser General Public License version 3",
"GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007"
),
"MIT": (
"The MIT License",
"including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software",
),
"MPL-2.0": (
"Mozilla Public License 2.0",
"https://opensource.org/licenses/MPL-2.0",
),
}
self._load_license()
if self.license_data:
for license, test_data in known_strings.items():
if license in self.license_data:
self.metadata.License = {"name":license,"file":self.license_file}
return
for test_text in test_data:
if test_text in self.license_data
self.metadata.License = {"name":license,"file":self.license_file}
return
def _load_readme(self):
""" Load in any existing readme """
valid_names = ["README.md", "README.txt", "README"]
for name in valid_names:
full_path = os.path.join(self.path,name)
if os.path.exists(full_path):
with open(full_path,"r",encoding="utf-8") as f:
self.readme_data = f.read()
return
def _load_license(self):
""" Load in any existing license """
valid_names = ["LICENSE", "LICENCE", "COPYING","LICENSE.txt", "LICENCE.txt", "COPYING.txt"]
for name in valid_names:
full_path = os.path.join(self.path,name)
if os.path.exists(full_path):
with open(full_path,"r",encoding="utf-8") as f:
self.license_data = f.read()
self.license_file = name
return

View File

@@ -0,0 +1,145 @@
# ***************************************************************************
# * *
# * Copyright (c) 2022 FreeCAD Project Association *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD is free software: you can redistribute it and/or modify it *
# * under the terms of the GNU Lesser General Public License as *
# * published by the Free Software Foundation, either version 2.1 of the *
# * License, or (at your option) any later version. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, but *
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
# * Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Lesser General Public *
# * License along with FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
""" Validators used for various line edits """
import keyword
from PySide2.QtGui import (
QValidator,
QRegularExpressionValidator,
)
from PySide2.QtCore import QRegularExpression
def isidentifier(ident: str) -> bool:
if not ident.isidentifier():
return False
if keyword.iskeyword(ident):
return False
return True
class NameValidator(QValidator):
"""Simple validator to exclude characters that are not valid in filenames."""
invalid = '/\\?%*:|"<>'
def validate(self, value: str, _: int):
"""Check the value against the validator"""
for char in value:
if char in NameValidator.invalid:
return QValidator.Invalid
return QValidator.Acceptable
def fixup(self, value: str) -> str:
"""Remove invalid characters from value"""
result = ""
for char in value:
if char not in NameValidator.invalid:
result += char
return result
class PythonIdentifierValidator(QValidator):
""" Validates whether input is a valid Python identifier. """
def validate(self, value: str, _: int):
if not value:
return QValidator.Intermediate
if not value.isidentifier():
return QValidator.Invalid # Includes an illegal character of some sort
if keyword.iskeyword(value):
return QValidator.Intermediate # They can keep typing and it might become valid
return QValidator.Acceptable
class SemVerValidator(QRegularExpressionValidator):
"""Implements the officially-recommended regex validator for Semantic version numbers."""
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
semver_re = QRegularExpression(
r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
+ r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
+ r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
+ r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
)
def __init__(self):
super().__init__()
self.setRegularExpression(SemVerValidator.semver_re)
@classmethod
def check(cls, value: str) -> bool:
"""Returns true if value validates, and false if not"""
return cls.semver_re.match(value).hasMatch()
class CalVerValidator(QRegularExpressionValidator):
"""Implements a basic regular expression validator that makes sure an entry corresponds
to a CalVer version numbering standard."""
calver_re = QRegularExpression(
r"^(?P<major>[1-9]\d{3})\.(?P<minor>[0-9]{1,2})\.(?P<patch>0|[0-9]{0,2})"
+ r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
+ r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
+ r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
)
def __init__(self):
super().__init__()
self.setRegularExpression(CalVerValidator.calver_re)
@classmethod
def check(cls, value: str) -> bool:
"""Returns true if value validates, and false if not"""
return cls.calver_re.match(value).hasMatch()
class VersionValidator(QValidator):
"""Implements the officially-recommended regex validator for Semantic version numbers, and a
decent approximation of the same thing for CalVer-style version numbers."""
def __init__(self):
super().__init__()
self.semver = SemVerValidator()
self.calver = CalVerValidator()
def validate(self, value: str, position: int):
"""Called for validation, returns a tuple of the validation state, the value, and the
position."""
semver_result = self.semver.validate(value, position)
calver_result = self.calver.validate(value, position)
if semver_result[0] == QValidator.Acceptable:
return semver_result
if calver_result[0] == QValidator.Acceptable:
return calver_result
if semver_result[0] == QValidator.Intermediate:
return semver_result
if calver_result[0] == QValidator.Intermediate:
return calver_result
return (QValidator.Invalid, value, position)

View File

@@ -73,7 +73,7 @@
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="Macro">
<layout class="QHBoxLayout" name="horizontalLayout_3">
@@ -263,7 +263,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QPlainTextEdit" name="plainTextEdit">
<widget class="QPlainTextEdit" name="descriptionTextEdit">
<property name="tabChangesFocus">
<bool>true</bool>
</property>