diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a6a48b5..7454cb1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,8 @@ Please consult the changelog to inform yourself about breaking changes and secur
* added separate `documentation` URL to Project metadata model
* added support for Julia `Project.toml` file
-* added support for fortran `fpm.toml` file
+* added support for Fortran `fpm.toml` file
+* added support for Java `pom.xml` file
## [v0.3.1](https://github.com/Materials-Data-Science-and-Informatics/somesy/tree/v0.3.1) (2024-01-23) { id="0.3.1" }
diff --git a/README.md b/README.md
index 66d3363f..e94a620a 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,16 @@ authoritative** source for project metadata, which is used to update all
supported (and enabled) *target files*. You can find an overview of supported
formats further below.
-By default, `somesy` will create (if they did not exist) or update `CITATION.cff` and `codemeta.json` files in your repository. If you happen to use `pyproject.toml` (in Python projects), `package.json` (in JavaScript projects), `Project.toml` (in Julia projects), or `fpm.toml` (in Fortran projects) somesy would also update the respective information there.
+By default, `somesy` will create (if they did not exist) or update `CITATION.cff` and `codemeta.json` files in your repository.
+If you happen to use
+
+* `pyproject.toml` (in Python projects),
+* `package.json` (in JavaScript projects),
+* `Project.toml` (in Julia projects),
+* `fpm.toml` (in Fortran projects),
+* `pom.xml` (in Java projects),
+
+then somesy would also update the respective information there.
You can see call available options with `somesy --help`,
all of these can also be conveniently set in your `somesy.toml` file.
@@ -171,15 +180,17 @@ Here is an overview of all the currently supported files and formats.
| package.json | ✓ | | package.json _(JavaScript)_ | ✓(2.) |
| Project.toml | ✓ | | Project.toml _(Julia)_ | ✓ |
| fpm.toml | ✓ | | fpm.toml _(Fortran)_ | ✓(3.) |
+| | ✓ | | pom.toml _(Java)_ | ✓(4.) |
| | | | CITATION.cff | ✓ |
-| | | | codemeta.json | ✓(4.) |
+| | | | codemeta.json | ✓(5.) |
**Notes:**
1. note that `somesy` does not support setuptools *dynamic fields*
2. `package.json` only supports one author, so `somesy` will pick the *first* listed author
3. `fpm.toml` only supports one author and maintainer, so `somesy` will pick the *first* listed author and maintainer
-4. unlike other targets, `somesy` will *re-create* the `codemeta.json` (i.e. do not edit it by hand!)
+4. `pom.xml` has no concept of `maintainers`, but it can have multiple licenses (somesy only supports one main project license)
+5. unlike other targets, `somesy` will *re-create* the `codemeta.json` (i.e. do not edit it by hand!)
diff --git a/codemeta.json b/codemeta.json
index 64e87022..04bd7297 100644
--- a/codemeta.json
+++ b/codemeta.json
@@ -46,20 +46,6 @@
"codeRepository": "https://github.com/Materials-Data-Science-and-Informatics/somesy",
"buildInstructions": "https://materials-data-science-and-informatics.github.io/somesy",
"contributor": [
- {
- "@type": "Person",
- "givenName": "Mustafa",
- "familyName": "Soylu",
- "email": "m.soylu@fz-juelich.de",
- "@id": "https://orcid.org/0000-0003-2637-0432"
- },
- {
- "@type": "Person",
- "givenName": "Anton",
- "familyName": "Pirogov",
- "email": "a.pirogov@fz-juelich.de",
- "@id": "https://orcid.org/0000-0002-5077-7497"
- },
{
"@type": "Person",
"givenName": "Jens",
diff --git a/docs/manual.md b/docs/manual.md
index ed2f29be..eb3882bf 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -117,33 +117,33 @@ some of the currently supported formats. Bold field names are mandatory, the oth
=== "Person Metadata"
- | Somesy Field | Poetry Config | SetupTools Config | Julia Config | Fortran Config | package.json | CITATION.cff | CodeMeta |
- | ---------------- | ------------- | ----------------- | ------------ | -------------- | ------------ | --------------- | --------------- |
- | | | | | | | | |
- | **given-names** | name+email | name | name+email | name+email | name | given-names | givenName |
- | **family-names** | name+email | name | name+email | name+email | name | family-names | familyName |
- | **email** | name+email | email | name+email | name+email | email | email | email |
- | orcid | - | - | - | - | url | orcid | id |
- | *(many others)* | - | - | - | - | - | *(same)* | *(same)* |
+ | Somesy Field | Poetry Config | SetupTools Config | Java POM | Julia Config | Fortran Config | package.json | CITATION.cff | CodeMeta |
+ | ---------------- | ------------- | ----------------- | ------------ | ------------ | -------------- | ------------ | --------------- | --------------- |
+ | | | | | | | | | |
+ | **given-names** | name+email | name | name | name+email | name+email | name | given-names | givenName |
+ | **family-names** | name+email | name | name | name+email | name+email | name | family-names | familyName |
+ | **email** | name+email | email | email | name+email | name+email | email | email | email |
+ | orcid | - | - | url | - | - | url | orcid | id |
+ | *(many others)* | - | - | - | - | - | - | *(same)* | *(same)* |
=== "Project Metadata"
- | Somesy Field | Poetry Config | SetupTools Config | Julia Config | Fortran Config | package.json | CITATION.cff | CodeMeta |
- | ----------------- | ------------- | ------------------ | ------------ | -------------- | ------------ | --------------- | ----------------- |
- | | | | | | | |
- | **name** | name | name | name | name | name | title | name |
- | **description** | description | description | - | description | description | abstract | description |
- | **license** | license | license | - | license | license | license | license |
- | **version** | version | version | version | version | version | version | version |
- | | | | | | | | |
- | ***author=true*** | authors | authors | authors | author | author | authors | author |
- | *maintainer=true* | maintainers | maintainers | - | maintainer | maintainers | contact | maintainer |
- | *people* | - | - | - | - | contributors | - | contributor |
- | | | | | | | | |
- | keywords | keywords | keywords | - | keywords | keywords | keywords | keywords |
- | homepage | homepage | urls.homepage | - | homepage | homepage | url | url |
- | repository | repository | urls.repository | - | - | repository | repository_code | codeRepository |
- | documentation | documentation | urls.documentation | - | - | - | - | buildInstructions |
+ | Somesy Field | Poetry Config | SetupTools Config | Java POM | Julia Config | Fortran Config | package.json | CITATION.cff | CodeMeta |
+ | ----------------- | ------------- | ------------------ | ------------------------------- | ------------ | -------------- | ------------ | --------------- | ----------------- |
+ | | | | | | | | | |
+ | **name** | name | name | name | name | name | name | title | name |
+ | **description** | description | description | description | - | description | description | abstract | description |
+ | **license** | license | license | licenses.license | - | license | license | license | license |
+ | **version** | version | version | version | version | version | version | version | version |
+ | | | | | | | | | |
+ | ***author=true*** | authors | authors | developers | authors | author | author | authors | author |
+ | *maintainer=true* | maintainers | maintainers | - | - | maintainer | maintainers | contact | maintainer |
+ | *people* | - | - | - | - | - | contributors | - | contributor |
+ | | | | | | | | | |
+ | keywords | keywords | keywords | - | - | keywords | keywords | keywords | keywords |
+ | homepage | homepage | urls.homepage | urls | - | homepage | homepage | url | url |
+ | repository | repository | urls.repository | scm.url | - | - | repository | repository_code | codeRepository |
+ | documentation | documentation | urls.documentation | distributionManagement.site.url | - | - | - | - | buildInstructions |
Note that the mapping is often not 1-to-1. For example, CITATION.cff allows rich
specification of author contact information and complex names. In contrast,
diff --git a/poetry.lock b/poetry.lock
index ecee0f5e..a6c6d042 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,10 +1,9 @@
-# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "annotated-types"
version = "0.6.0"
description = "Reusable constraint types to use with typing.Annotated"
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -19,7 +18,6 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""}
name = "anybadge"
version = "1.14.0"
description = "Simple, flexible badge generator for project badges."
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -34,7 +32,6 @@ packaging = "*"
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
-category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
@@ -52,7 +49,6 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy"
name = "babel"
version = "2.13.1"
description = "Internationalization utilities"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -71,7 +67,6 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
name = "black"
version = "23.11.0"
description = "The uncompromising code formatter."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -114,7 +109,6 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "certifi"
version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -126,7 +120,6 @@ files = [
name = "cffconvert"
version = "2.0.0"
description = "Command line program to validate and convert CITATION.cff files."
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -150,7 +143,6 @@ publishing = ["twine", "wheel"]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -162,7 +154,6 @@ files = [
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@@ -262,7 +253,6 @@ files = [
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -277,7 +267,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
-category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@@ -289,7 +278,6 @@ files = [
name = "commonmark"
version = "0.9.1"
description = "Python parser for the CommonMark Markdown spec"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -304,7 +292,6 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
name = "coverage"
version = "7.3.2"
description = "Code coverage measurement for Python"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -368,11 +355,21 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
[package.extras]
toml = ["tomli"]
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+
[[package]]
name = "distlib"
version = "0.3.7"
description = "Distribution utilities"
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -384,7 +381,6 @@ files = [
name = "dnspython"
version = "2.4.2"
description = "DNS toolkit"
-category = "main"
optional = false
python-versions = ">=3.8,<4.0"
files = [
@@ -404,7 +400,6 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -415,7 +410,6 @@ files = [
name = "email-validator"
version = "2.1.0.post1"
description = "A robust email address syntax and deliverability validation library."
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -431,7 +425,6 @@ idna = ">=2.0.0"
name = "exceptiongroup"
version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -446,7 +439,6 @@ test = ["pytest (>=6)"]
name = "fhconfparser"
version = "2023"
description = "Provides a config language independent way to read a config file."
-category = "dev"
optional = false
python-versions = ">=3.8,<4.0"
files = [
@@ -462,7 +454,6 @@ tomli = ">=2.0.0,<3"
name = "filelock"
version = "3.13.1"
description = "A platform independent file lock."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -479,7 +470,6 @@ typing = ["typing-extensions (>=4.8)"]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -497,7 +487,6 @@ dev = ["flake8", "markdown", "twine", "wheel"]
name = "griffe"
version = "0.38.1"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -512,7 +501,6 @@ colorama = ">=0.4"
name = "hypothesis"
version = "6.91.0"
description = "A library for property-based testing"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -545,7 +533,6 @@ zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2023.3)"]
name = "identify"
version = "2.5.33"
description = "File identification library for Python"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -560,7 +547,6 @@ license = ["ukkonen"]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -572,7 +558,6 @@ files = [
name = "importlib-metadata"
version = "4.13.0"
description = "Read metadata from Python packages"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -592,7 +577,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag
name = "importlib-resources"
version = "6.1.1"
description = "Read resources from Python packages"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -611,7 +595,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)",
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -623,7 +606,6 @@ files = [
name = "interrogate"
version = "1.5.0"
description = "Interrogate a codebase for docstring coverage."
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -649,7 +631,6 @@ tests = ["pytest", "pytest-cov", "pytest-mock"]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -667,7 +648,6 @@ i18n = ["Babel (>=2.7)"]
name = "jsonschema"
version = "3.2.0"
description = "An implementation of JSON Schema validation for Python"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -689,7 +669,6 @@ format-nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-va
name = "licensecheck"
version = "2023.1.2"
description = "Output the licenses used by dependencies and check if these are compatible with the project license"
-category = "dev"
optional = false
python-versions = ">=3.8,<4.0"
files = [
@@ -708,7 +687,6 @@ tomli = ">=2.0.1,<3"
name = "markdown"
version = "3.5.1"
description = "Python implementation of John Gruber's Markdown."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -727,7 +705,6 @@ testing = ["coverage", "pyyaml"]
name = "markdown-exec"
version = "1.7.0"
description = "Utilities to execute code blocks in Markdown files."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -746,7 +723,6 @@ ansi = ["pygments-ansi-color"]
name = "markdown-include"
version = "0.8.1"
description = "A Python-Markdown extension which provides an 'include' function"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -764,7 +740,6 @@ tests = ["pytest"]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -824,7 +799,6 @@ files = [
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -836,7 +810,6 @@ files = [
name = "mike"
version = "2.0.0"
description = "Manage multiple versions of your MkDocs-powered documentation"
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -861,7 +834,6 @@ test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"]
name = "mkdocs"
version = "1.5.3"
description = "Project documentation with Markdown."
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -893,7 +865,6 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
name = "mkdocs-autorefs"
version = "0.5.0"
description = "Automatically link across pages in MkDocs."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -909,7 +880,6 @@ mkdocs = ">=1.1"
name = "mkdocs-coverage"
version = "1.0.0"
description = "MkDocs plugin to integrate your coverage HTML report into your site."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -924,7 +894,6 @@ mkdocs = ">=1.2"
name = "mkdocs-exclude"
version = "1.0.2"
description = "A mkdocs plugin that lets you exclude files or trees."
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -938,7 +907,6 @@ mkdocs = "*"
name = "mkdocs-gen-files"
version = "0.5.0"
description = "MkDocs plugin to programmatically generate documentation pages during the build"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -953,7 +921,6 @@ mkdocs = ">=1.0.3"
name = "mkdocs-literate-nav"
version = "0.6.1"
description = "MkDocs plugin to specify the navigation in Markdown instead of YAML"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -968,7 +935,6 @@ mkdocs = ">=1.0.3"
name = "mkdocs-macros-plugin"
version = "0.7.0"
description = "Unleash the power of MkDocs with macros and variables"
-category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@@ -990,7 +956,6 @@ test = ["mkdocs-include-markdown-plugin", "mkdocs-macros-test", "mkdocs-material
name = "mkdocs-material"
version = "9.5.0"
description = "Documentation that simply works"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1020,7 +985,6 @@ recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.
name = "mkdocs-material-extensions"
version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1032,7 +996,6 @@ files = [
name = "mkdocs-section-index"
version = "0.3.8"
description = "MkDocs plugin to allow clickable sections that lead to an index page"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1047,7 +1010,6 @@ mkdocs = ">=1.2"
name = "mkdocstrings"
version = "0.24.0"
description = "Automatic documentation from sources, for MkDocs."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1077,7 +1039,6 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
name = "mkdocstrings-python"
version = "1.7.5"
description = "A Python handler for mkdocstrings."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1093,7 +1054,6 @@ mkdocstrings = ">=0.20"
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
-category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@@ -1105,7 +1065,6 @@ files = [
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
-category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
@@ -1120,7 +1079,6 @@ setuptools = "*"
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1132,7 +1090,6 @@ files = [
name = "paginate"
version = "0.5.6"
description = "Divides large result sets into pages for easier browsing"
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -1143,7 +1100,6 @@ files = [
name = "pastel"
version = "0.2.1"
description = "Bring colors to your terminal."
-category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@@ -1155,7 +1111,6 @@ files = [
name = "pathspec"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1167,7 +1122,6 @@ files = [
name = "platformdirs"
version = "4.1.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1183,7 +1137,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
name = "pluggy"
version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1199,7 +1152,6 @@ testing = ["pytest", "pytest-benchmark"]
name = "poethepoet"
version = "0.18.1"
description = "A task runner that works well with poetry."
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1218,7 +1170,6 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1237,7 +1188,6 @@ virtualenv = ">=20.10.0"
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
-category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
@@ -1249,7 +1199,6 @@ files = [
name = "pydantic"
version = "2.5.2"
description = "Data validation using Python type hints"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1270,7 +1219,6 @@ email = ["email-validator (>=2.0.0)"]
name = "pydantic-core"
version = "2.14.5"
description = ""
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1388,7 +1336,6 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
name = "pygments"
version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1404,7 +1351,6 @@ windows-terminal = ["colorama (>=0.4.6)"]
name = "pygments-ansi-color"
version = "0.3.0"
description = ""
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1419,7 +1365,6 @@ pygments = "!=2.7.3"
name = "pykwalify"
version = "1.8.0"
description = "Python lib/cli for JSON/YAML schema validation"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -1436,7 +1381,6 @@ python-dateutil = ">=2.8.0"
name = "pymdown-extensions"
version = "10.5"
description = "Extension pack for Python Markdown."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1455,7 +1399,6 @@ extra = ["pygments (>=2.12)"]
name = "pyparsing"
version = "3.1.1"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-category = "dev"
optional = false
python-versions = ">=3.6.8"
files = [
@@ -1470,7 +1413,6 @@ diagrams = ["jinja2", "railroad-diagrams"]
name = "pyrsistent"
version = "0.20.0"
description = "Persistent/Functional/Immutable data structures"
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -1512,7 +1454,6 @@ files = [
name = "pytest"
version = "7.4.3"
description = "pytest: simple powerful testing with Python"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1535,7 +1476,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
name = "pytest-cov"
version = "4.1.0"
description = "Pytest plugin for measuring coverage."
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1554,7 +1494,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
name = "pytest-mock"
version = "3.12.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1572,7 +1511,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
-category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
@@ -1587,7 +1525,6 @@ six = ">=1.5"
name = "pytz"
version = "2023.3.post1"
description = "World timezone definitions, modern and historical"
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -1599,7 +1536,6 @@ files = [
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -1649,7 +1585,6 @@ files = [
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -1664,7 +1599,6 @@ pyyaml = "*"
name = "regex"
version = "2023.10.3"
description = "Alternative regular expression module, to replace re."
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1762,7 +1696,6 @@ files = [
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1784,7 +1717,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "requirements-parser"
version = "0.5.0"
description = "This is a small Python module for parsing Pip requirement files."
-category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
files = [
@@ -1799,7 +1731,6 @@ types-setuptools = ">=57.0.0"
name = "rich"
version = "12.6.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-category = "main"
optional = false
python-versions = ">=3.6.3,<4.0.0"
files = [
@@ -1819,7 +1750,6 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
name = "ruamel-yaml"
version = "0.17.40"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
-category = "main"
optional = false
python-versions = ">=3"
files = [
@@ -1838,57 +1768,36 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
name = "ruamel-yaml-clib"
version = "0.2.8"
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"},
- {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"},
- {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"},
- {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"},
- {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"},
- {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"},
- {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"},
- {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"},
- {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"},
- {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"},
- {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"},
- {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"},
{file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"},
- {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"},
- {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"},
- {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"},
- {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"},
- {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"},
- {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"},
- {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"},
- {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"},
- {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"},
{file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"},
@@ -1898,7 +1807,6 @@ files = [
name = "setuptools"
version = "69.0.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -1915,7 +1823,6 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar
name = "shellingham"
version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1927,7 +1834,6 @@ files = [
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
-category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -1939,7 +1845,6 @@ files = [
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -1951,7 +1856,6 @@ files = [
name = "tabulate"
version = "0.9.0"
description = "Pretty-print tabular data"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1966,7 +1870,6 @@ widechars = ["wcwidth"]
name = "termcolor"
version = "2.4.0"
description = "ANSI color formatting for output in terminal"
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -1981,7 +1884,6 @@ tests = ["pytest", "pytest-cov"]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -1993,7 +1895,6 @@ files = [
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -2005,7 +1906,6 @@ files = [
name = "tomlkit"
version = "0.11.8"
description = "Style preserving TOML library"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -2017,7 +1917,6 @@ files = [
name = "typer"
version = "0.7.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -2041,7 +1940,6 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.
name = "types-setuptools"
version = "69.0.0.0"
description = "Typing stubs for setuptools"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -2053,7 +1951,6 @@ files = [
name = "typing-extensions"
version = "4.8.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -2065,7 +1962,6 @@ files = [
name = "urllib3"
version = "2.1.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -2082,7 +1978,6 @@ zstd = ["zstandard (>=0.18.0)"]
name = "verspec"
version = "0.1.0"
description = "Flexible version handling"
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -2097,7 +1992,6 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"]
name = "virtualenv"
version = "20.25.0"
description = "Virtual Python Environment builder"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -2118,7 +2012,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
name = "watchdog"
version = "3.0.0"
description = "Filesystem events monitoring"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -2158,7 +2051,6 @@ watchmedo = ["PyYAML (>=3.10)"]
name = "wrapt"
version = "1.16.0"
description = "Module for decorators, wrappers and monkey patching."
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -2238,7 +2130,6 @@ files = [
name = "zipp"
version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -2253,4 +2144,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "7ba6685496db93e8a6ad458a07133eaf736168e4a313c3d80a1dd07d5a0b3b87"
+content-hash = "6bc518881b74a7e46d4a5fbe0b3a893f2736ba9842f7f5ccb958a49f07f792bf"
diff --git a/pyproject.toml b/pyproject.toml
index 1df864cf..4d9ff32b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,6 +44,7 @@ cffconvert = "^2.0.0"
wrapt = "^1.15.0"
packaging = "^23.1"
jinja2 = "^3.1.2"
+defusedxml = "^0.7.1"
[tool.poetry.group.dev.dependencies]
poethepoet = "^0.18.1"
diff --git a/src/somesy/cli/fill.py b/src/somesy/cli/fill.py
index 857ac7eb..a0f87059 100644
--- a/src/somesy/cli/fill.py
+++ b/src/somesy/cli/fill.py
@@ -6,7 +6,12 @@
import typer
from jinja2 import Environment, FunctionLoader, select_autoescape
-from .util import resolved_somesy_input, wrap_exceptions
+from .util import (
+ existing_file_arg_config,
+ file_arg_config,
+ resolved_somesy_input,
+ wrap_exceptions,
+)
logger = logging.getLogger("somesy")
app = typer.Typer()
@@ -19,37 +24,22 @@ def fill(
None,
"--template",
"-t",
- exists=True,
- file_okay=True,
- dir_okay=False,
- writable=False,
- readable=True,
- resolve_path=False,
help="Path to a Jinja2 template for somesy to fill (default: stdin).",
+ **existing_file_arg_config,
),
input_file: Path = typer.Option(
None,
"--input-file",
"-i",
- exists=True,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
help="Path of somesy input file (default: try to infer).",
+ **existing_file_arg_config,
),
output_file: Path = typer.Option(
None,
"--output-file",
"-o",
- exists=False,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=False,
- resolve_path=True,
help="Path for target file (default: stdout).",
+ **file_arg_config,
),
):
"""Fill a Jinja2 template with somesy project metadata (e.g. list authors in project docs)."""
diff --git a/src/somesy/cli/init.py b/src/somesy/cli/init.py
index 9020b912..6e5936e4 100644
--- a/src/somesy/cli/init.py
+++ b/src/somesy/cli/init.py
@@ -23,44 +23,46 @@ def config():
# prompt for inputs
input_file = typer.prompt("Input file path", default=input_file_default)
- input_file = Path(input_file)
- options = {
- "input_file": input_file,
- "no_sync_cff": not typer.confirm(
- "Do you want to sync to a CFF file?", default=True
- ),
- }
- cff_file = typer.prompt("CFF file path", default="CITATION.cff")
- if cff_file is not None or cff_file != "":
+ options = dict(input_file=Path(input_file))
+
+ # ----
+
+ options["no_sync_cff"] = not typer.confirm(
+ "Do you want to sync to a CFF file?", default=True
+ )
+ if cff_file := typer.prompt("CFF file path", default="CITATION.cff"):
options["cff_file"] = cff_file
+ options["no_sync_codemeta"] = not typer.confirm(
+ "Do you want to sync to a codemeta.json file?", default=True
+ )
+ if codemeta_file := typer.prompt(
+ "codemeta.json file path", default="codemeta.json"
+ ):
+ options["codemeta_file"] = codemeta_file
+
options["no_sync_pyproject"] = not typer.confirm(
"Do you want to sync to a pyproject.toml file?", default=True
)
-
- pyproject_file = typer.prompt("pyproject.toml file path", default="pyproject.toml")
- if pyproject_file is not None or pyproject_file != "":
+ if pyproject_file := typer.prompt(
+ "pyproject.toml file path", default="pyproject.toml"
+ ):
options["pyproject_file"] = pyproject_file
options["sync_package_json"] = typer.confirm(
"Do you want to sync to a package.json file?", default=False
)
- package_json_file = typer.prompt("package.json file path", default="package.json")
- if package_json_file is not None or package_json_file != "":
+ if package_json_file := typer.prompt(
+ "package.json file path", default="package.json"
+ ):
options["package_json_file"] = package_json_file
- options["no_sync_codemeta"] = not typer.confirm(
- "Do you want to sync to a codemeta.json file?", default=True
- )
- codemeta_file = typer.prompt("codemeta.json file path", default="codemeta.json")
- if codemeta_file is not None or codemeta_file != "":
- options["codemeta_file"] = codemeta_file
-
options["no_sync_julia"] = not typer.confirm(
"Do you want to sync to a Project.toml(Julia) file?", default=True
)
- julia_file = typer.prompt("Project.toml(Julia) file path", default="Project.toml")
- if julia_file is not None or julia_file != "":
+ if julia_file := typer.prompt(
+ "Project.toml (Julia) file path", default="Project.toml"
+ ):
options["julia_file"] = julia_file
options["no_sync_fortran"] = not typer.confirm(
@@ -70,6 +72,14 @@ def config():
if fortran_file is not None or fortran_file != "":
options["fortran_file"] = fortran_file
+ options["no_sync_pom_xml"] = not typer.confirm(
+ "Do you want to sync to a pom.xml file?", default=True
+ )
+ if pom_xml_file := typer.prompt("pom.xml file path", default="pom.xml"):
+ options["pom_xml_file"] = pom_xml_file
+
+ # ----
+
options["show_info"] = typer.confirm(
"Do you want to show info about the sync process?"
)
diff --git a/src/somesy/cli/sync.py b/src/somesy/cli/sync.py
index 67b127ad..c2eb3b48 100644
--- a/src/somesy/cli/sync.py
+++ b/src/somesy/cli/sync.py
@@ -7,7 +7,12 @@
from somesy.commands import sync as sync_command
from somesy.core.models import SomesyInput
-from .util import resolved_somesy_input, wrap_exceptions
+from .util import (
+ existing_file_arg_config,
+ file_arg_config,
+ resolved_somesy_input,
+ wrap_exceptions,
+)
logger = logging.getLogger("somesy")
@@ -21,13 +26,8 @@ def sync(
None,
"--input-file",
"-i",
- exists=False,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
help="Somesy input file path (default: .somesy.toml)",
+ **file_arg_config,
),
no_sync_pyproject: bool = typer.Option(
None,
@@ -39,13 +39,8 @@ def sync(
None,
"--pyproject-file",
"-p",
- exists=True,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
help="Existing pyproject.toml file path (default: pyproject.toml)",
+ **existing_file_arg_config,
),
no_sync_package_json: bool = typer.Option(
None,
@@ -57,13 +52,47 @@ def sync(
None,
"--package-json-file",
"-j",
- exists=True,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
help="Existing package.json file path (default: package.json)",
+ **existing_file_arg_config,
+ ),
+ no_sync_julia: bool = typer.Option(
+ None,
+ "--no-sync-julia",
+ "-L",
+ help="Do not sync Project.toml (Julia) file (default: False)",
+ ),
+ julia_file: Path = typer.Option(
+ None,
+ "--julia-file",
+ "-l",
+ help="Custom Project.toml (Julia) file path (default: Project.toml)",
+ **existing_file_arg_config,
+ ),
+ no_sync_fortran: bool = typer.Option(
+ None,
+ "--no-sync-fortran",
+ "-F",
+ help="Do not sync fpm.toml (Fortran) file (default: False)",
+ **existing_file_arg_config,
+ ),
+ fortran_file: Path = typer.Option(
+ None,
+ "--fortran-file",
+ "-f",
+ help="Custom fpm.toml (Fortran) file path (default: fpm.toml)",
+ ),
+ no_sync_pom_xml: bool = typer.Option(
+ None,
+ "--no-sync-pomxml",
+ "-X",
+ help="Do not sync pom.xml (Java Maven) file (default: False)",
+ ),
+ pom_xml_file: Path = typer.Option(
+ None,
+ "--pomxml-file",
+ "-x",
+ help="Custom pom.xml (Java Maven) file path (default: pom.xml)",
+ **existing_file_arg_config,
),
no_sync_cff: bool = typer.Option(
None,
@@ -75,67 +104,21 @@ def sync(
None,
"--cff-file",
"-c",
- exists=False,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
help="CITATION.cff file path (default: CITATION.cff)",
+ **file_arg_config,
),
no_sync_codemeta: bool = typer.Option(
None,
"--no-sync-codemeta",
"-M",
- help="Do not sync codemeta.json file",
+ help="Do not sync codemeta.json file (default: False)",
),
codemeta_file: Path = typer.Option(
None,
"--codemeta-file",
"-m",
- exists=False,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
- help="Custom codemeta.json file path",
- ),
- no_sync_julia: bool = typer.Option(
- None,
- "--no-sync-julia",
- "-L",
- help="Do not sync Project.toml(Julia) file (default: False)",
- ),
- julia_file: Path = typer.Option(
- None,
- "--julia-file",
- "-l",
- exists=True,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
- help="Custom Project.toml(Julia) file path",
- ),
- no_sync_fortran: bool = typer.Option(
- None,
- "--no-sync-fortran",
- "-F",
- help="Do not sync fpm.toml(fortran) file (default: False)",
- ),
- fortran_file: Path = typer.Option(
- None,
- "--fortran-file",
- "-f",
- exists=True,
- file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True,
- resolve_path=True,
- help="Custom fpm.toml(fortran) file path",
+ help="Custom codemeta.json file path (default: codemeta.json)",
+ **file_arg_config,
),
):
"""Sync project metadata input with metadata files."""
@@ -153,6 +136,8 @@ def sync(
julia_file=julia_file,
no_sync_fortran=no_sync_fortran,
fortran_file=fortran_file,
+ no_sync_pom_xml=no_sync_pom_xml,
+ pom_xml_file=pom_xml_file,
)
run_sync(somesy_input)
@@ -170,20 +155,25 @@ def run_sync(somesy_input: SomesyInput):
logger.info(
f" - [italic]package.json[/italic]:\t[grey]{conf.package_json_file}[/grey]"
)
- if not conf.no_sync_cff:
- logger.info(f" - [italic]CITATION.cff[/italic]:\t[grey]{conf.cff_file}[/grey]")
- if not conf.no_sync_codemeta:
- logger.info(
- f" - [italic]codemeta.json[/italic]:\t[grey]{conf.codemeta_file}[/grey]\n"
- )
if not conf.no_sync_julia:
logger.info(
- f" - [italic]Project.toml(Julia)[/italic]:\t[grey]{conf.julia_file}[/grey]"
+ f" - [italic]Project.toml[/italic]:\t[grey]{conf.julia_file}[/grey]\n"
)
if not conf.no_sync_fortran:
logger.info(
f" - [italic]fpm.toml(fortran)[/italic]:\t[grey]{conf.fortran_file}[/grey]"
)
+ if not conf.no_sync_pom_xml:
+ logger.info(
+ f" - [italic]pom.xml[/italic]:\t[grey]{conf.pom_xml_file}[/grey]\n"
+ )
+
+ if not conf.no_sync_cff:
+ logger.info(f" - [italic]CITATION.cff[/italic]:\t[grey]{conf.cff_file}[/grey]")
+ if not conf.no_sync_codemeta:
+ logger.info(
+ f" - [italic]codemeta.json[/italic]:\t[grey]{conf.codemeta_file}[/grey]\n"
+ )
# ----
sync_command(somesy_input)
# ----
diff --git a/src/somesy/cli/util.py b/src/somesy/cli/util.py
index 9e50cb66..954067a1 100644
--- a/src/somesy/cli/util.py
+++ b/src/somesy/cli/util.py
@@ -14,6 +14,18 @@
logger = logging.getLogger("somesy")
+# configuration dicts for CLI file arguments
+file_arg_config = dict(
+ file_okay=True,
+ dir_okay=False,
+ writable=True,
+ readable=True,
+ resolve_path=True,
+)
+existing_file_arg_config = dict(file_arg_config)
+existing_file_arg_config.update(dict(exists=True))
+
+
@wrapt.decorator
def wrap_exceptions(wrapped, instance, args, kwargs):
"""Format and log exceptions for cli commands."""
diff --git a/src/somesy/codemeta/__init__.py b/src/somesy/codemeta/__init__.py
index 0c70c4c5..eda81065 100644
--- a/src/somesy/codemeta/__init__.py
+++ b/src/somesy/codemeta/__init__.py
@@ -1,4 +1,4 @@
"""Integration with codemeta.json (to re-generate codemeta as part of somesy sync)."""
-from .writer import Codemeta
+from .writer import CodeMeta
-__all__ = ["Codemeta"]
+__all__ = ["CodeMeta"]
diff --git a/src/somesy/codemeta/writer.py b/src/somesy/codemeta/writer.py
index 654f36a3..02e21559 100644
--- a/src/somesy/codemeta/writer.py
+++ b/src/somesy/codemeta/writer.py
@@ -13,7 +13,7 @@
logger = logging.getLogger("somesy")
-class Codemeta(ProjectMetadataWriter):
+class CodeMeta(ProjectMetadataWriter):
"""Codemeta.json parser and saver."""
def __init__(
@@ -71,7 +71,7 @@ def _validate(self) -> None:
config = dict(self._get_property([]))
logger.debug(
- f"No validation for codemeta.json files {Codemeta.__name__}: {pretty_repr(config)}"
+ f"No validation for codemeta.json files {CodeMeta.__name__}: {pretty_repr(config)}"
)
def _init_new_file(self) -> None:
@@ -167,4 +167,6 @@ def sync(self, metadata: ProjectMetadata) -> None:
Use existing sync function from ProjectMetadataWriter but update repository and contributors.
"""
super().sync(metadata)
- self.contributors = self._sync_person_list(self.contributors, metadata.people)
+ self.contributors = self._sync_person_list(
+ self.contributors, metadata.contributors()
+ )
diff --git a/src/somesy/commands/sync.py b/src/somesy/commands/sync.py
index bdc4804a..d262a4ef 100644
--- a/src/somesy/commands/sync.py
+++ b/src/somesy/commands/sync.py
@@ -1,153 +1,63 @@
"""Sync selected metadata files with given input file."""
import logging
from pathlib import Path
+from typing import Type
from rich.pretty import pretty_repr
from somesy.cff.writer import CFF
-from somesy.codemeta import Codemeta
+from somesy.codemeta import CodeMeta
from somesy.core.models import ProjectMetadata, SomesyInput
+from somesy.core.writer import ProjectMetadataWriter
from somesy.fortran.writer import Fortran
from somesy.julia.writer import Julia
from somesy.package_json.writer import PackageJSON
+from somesy.pom_xml.writer import POM
from somesy.pyproject.writer import Pyproject
logger = logging.getLogger("somesy")
+def _sync_file(
+ metadata: ProjectMetadata, file: Path, writer_cls: Type[ProjectMetadataWriter]
+):
+ """Sync metadata to a file using the provided writer."""
+ logger.verbose(f"Loading '{file.name}' ...")
+ writer = writer_cls(file)
+ logger.verbose(f"Syncing '{file.name}' ...")
+ writer.sync(metadata)
+ writer.save(file)
+ logger.verbose(f"Saved synced '{file.name}'.\n")
+
+
def sync(somesy_input: SomesyInput):
"""Sync selected metadata files with given input file."""
conf, metadata = somesy_input.config, somesy_input.project
- logger.debug(
- f"Project metadata: {pretty_repr(metadata.model_dump(exclude_defaults=True))}"
- )
+ pp_metadata = pretty_repr(metadata.model_dump(exclude_defaults=True))
+ logger.debug(f"Project metadata: {pp_metadata}")
# update these only if they exist:
if conf.pyproject_file.is_file() and not conf.no_sync_pyproject:
- _sync_python(metadata, conf.pyproject_file)
+ _sync_file(metadata, conf.pyproject_file, Pyproject)
if conf.package_json_file.is_file() and not conf.no_sync_package_json:
- _sync_package_json(metadata, conf.package_json_file)
+ _sync_file(metadata, conf.package_json_file, PackageJSON)
- # create these by default if they are missing:
- if not conf.no_sync_cff:
- _sync_cff(metadata, conf.cff_file)
+ if conf.julia_file.is_file() and not conf.no_sync_julia:
+ _sync_file(metadata, conf.julia_file, Julia)
- if not conf.no_sync_codemeta:
- _sync_codemeta(metadata, conf.codemeta_file)
+ if conf.fortran_file.is_file() and not conf.no_sync_fortran:
+ _sync_file(metadata, conf.fortran_file, Fortran)
- if not conf.no_sync_julia:
- _sync_julia(metadata, conf.julia_file)
+ if conf.pom_xml_file.is_file() and not conf.no_sync_pom_xml:
+ _sync_file(metadata, conf.pom_xml_file, POM)
- if not conf.no_sync_fortran:
- _sync_fortran(metadata, conf.fortran_file)
+ # create these by default if they are missing:
+ if not conf.no_sync_cff:
+ _sync_file(metadata, conf.cff_file, CFF)
-def _sync_python(
- metadata: ProjectMetadata,
- pyproject_file: Path,
-):
- """Sync pyproject.toml file using project metadata.
-
- Args:
- metadata (ProjectMetadata): project metadata to sync pyproject.toml file.
- pyproject_file (Path, optional): pyproject file to read project metadata from.
- """
- logger.verbose("Loading pyproject.toml file.")
- pyproject = Pyproject(pyproject_file)
- logger.verbose("Syncing pyproject.toml file.")
- pyproject.sync(metadata)
- pyproject.save()
- logger.verbose("Saved synced pyproject.toml file.\n")
-
-
-def _sync_cff(
- metadata: ProjectMetadata,
- cff_file: Path,
-):
- """Sync CITATION.cff file using project metadata.
-
- Args:
- metadata (ProjectMetadata): project metadata to sync pyproject.toml file.
- cff_file (Path, optional): CFF file path if wanted to be synced. Defaults to None.
- """
- logger.verbose("Loading CITATION.cff file.")
- cff = CFF(cff_file)
- logger.verbose("Syncing CITATION.cff file.")
- cff.sync(metadata)
- cff.save()
- logger.verbose("Saved synced CITATION.cff file.\n")
-
-
-def _sync_package_json(
- metadata: ProjectMetadata,
- package_json_file: Path,
-):
- """Sync package.json file using project metadata.
-
- Args:
- metadata (ProjectMetadata): project metadata to sync pyproject.toml file.
- package_json_file (Path, optional): package.json file path if wanted to be synced. Defaults to None.
- """
- logger.verbose("Loading package.json file.")
- package_json = PackageJSON(package_json_file)
- logger.verbose("Syncing package.json file.")
- package_json.sync(metadata)
- package_json.save()
- logger.verbose("Saved synced package.json file.\n")
-
-
-def _sync_codemeta(
- metadata: ProjectMetadata,
- codemeta_file: Path,
-):
- """Sync codemeta.json file using project metadata.
-
- Args:
- metadata (ProjectMetadata): project metadata to sync pyproject.toml file.
- codemeta_file (Path, optional): codemeta.json file path if wanted to be synced. Defaults to None.
- """
- logger.verbose("Creating codemeta.json file.")
- cm = Codemeta(codemeta_file)
- logger.verbose("Syncing codemeta.json file.")
- cm.sync(metadata)
- cm.save()
- logger.verbose(f"New codemeta graph written to {codemeta_file}.")
-
-
-def _sync_julia(
- metadata: ProjectMetadata,
- julia_file: Path,
-):
- """Sync Project.toml file using project metadata.
-
- Args:
- metadata (ProjectMetadata): project metadata to sync Project.toml file.
- julia_file (Path, optional): Project.toml file path if wanted to be synced. Defaults to None.
- """
- logger.verbose("Loading Project.toml file.")
- cm = Julia(julia_file)
- logger.verbose("Syncing Project.toml file.")
- cm.sync(metadata)
- cm.save()
- logger.verbose(f"Saved synced Project.toml file to {julia_file}.")
-
-
-def _sync_fortran(
- metadata: ProjectMetadata,
- fortran_file: Path,
-):
- """Sync fpm.toml file using project metadata.
-
- Args:
- metadata (ProjectMetadata): project metadata to sync fpm.toml file.
- fortran_file (Path, optional): fpm.toml file path if wanted to be synced. Defaults to None.
- """
- logger.verbose("Loading fpm.toml file.")
- cm = Fortran(fortran_file)
- logger.verbose("Syncing fpm.toml file.")
- cm.sync(metadata)
- cm.save()
- logger.verbose(f"Saved synced fpm.toml file to {fortran_file}.")
+ if not conf.no_sync_codemeta:
+ _sync_file(metadata, conf.codemeta_file, CodeMeta)
diff --git a/src/somesy/core/models.py b/src/somesy/core/models.py
index 3032f45d..e8d13dfb 100644
--- a/src/somesy/core/models.py
+++ b/src/somesy/core/models.py
@@ -36,6 +36,7 @@ class SomesyBaseModel(BaseModel):
model_config = dict(
extra="forbid",
+ validate_assignment=True,
populate_by_name=True,
str_strip_whitespace=True,
str_min_length=1,
@@ -119,7 +120,15 @@ def model_dump_json(self, *args, **kwargs):
return json.dumps(ret, ensure_ascii=False)
-_SOMESY_TARGETS = ["cff", "pyproject", "package_json", "codemeta", "julia", "fortran"]
+_SOMESY_TARGETS = [
+ "cff",
+ "pyproject",
+ "package_json",
+ "codemeta",
+ "julia",
+ "fortran",
+ "pom_xml",
+]
class SomesyConfig(SomesyBaseModel):
@@ -171,18 +180,6 @@ def at_least_one_target(cls, values):
Path, Field(description="package.json file path.")
] = Path("package.json")
- no_sync_cff: Annotated[bool, Field(description="Do not sync with CFF.")] = False
- cff_file: Annotated[Path, Field(description="CFF file path.")] = Path(
- "CITATION.cff"
- )
-
- no_sync_codemeta: Annotated[
- bool, Field(description="Do not sync with codemeta.json.")
- ] = False
- codemeta_file: Annotated[
- Path, Field(description="codemeta.json file path.")
- ] = Path("codemeta.json")
-
no_sync_julia: Annotated[
bool, Field(description="Do not sync with Project.toml.")
] = False
@@ -197,6 +194,25 @@ def at_least_one_target(cls, values):
"fpm.toml"
)
+ no_sync_pom_xml: Annotated[
+ bool, Field(description="Do not sync with pom.xml.")
+ ] = False
+ pom_xml_file: Annotated[Path, Field(description="pom.xml file path.")] = Path(
+ "pom.xml"
+ )
+
+ no_sync_cff: Annotated[bool, Field(description="Do not sync with CFF.")] = False
+ cff_file: Annotated[Path, Field(description="CFF file path.")] = Path(
+ "CITATION.cff"
+ )
+
+ no_sync_codemeta: Annotated[
+ bool, Field(description="Do not sync with codemeta.json.")
+ ] = False
+ codemeta_file: Annotated[
+ Path, Field(description="codemeta.json file path.")
+ ] = Path("codemeta.json")
+
def log_level(self) -> SomesyLogLevel:
"""Return log level derived from this configuration."""
return SomesyLogLevel.from_flags(
diff --git a/src/somesy/core/writer.py b/src/somesy/core/writer.py
index 8b870cfa..6c25d749 100644
--- a/src/somesy/core/writer.py
+++ b/src/somesy/core/writer.py
@@ -16,6 +16,12 @@ class IgnoreKey:
FieldKeyMapping = Dict[str, Union[List[str], IgnoreKey]]
"""Type to be used for the dict passed as `direct_mappings`."""
+DictLike = Any
+"""Dict-like that supports getitem, setitem, delitem, etc.
+
+NOTE: This should be probably turned into a proper protocol.
+"""
+
class ProjectMetadataWriter(ABC):
"""Base class for Project Metadata Output Wrapper.
@@ -42,8 +48,8 @@ def __init__(
create_if_not_exists: Create an empty CFF file if not exists. Defaults to True.
direct_mappings: Dict with direct mappings of keys between somesy and target
"""
- self._data: Dict = {}
- self.path = path
+ self._data: DictLike = {}
+ self.path = path if isinstance(path, Path) else Path(path)
self.create_if_not_exists = create_if_not_exists
self.direct_mappings = direct_mappings or {}
@@ -94,7 +100,11 @@ def save(self, path: Optional[Path]) -> None:
"""
def _get_property(
- self, key: Union[str, List[str]], *, remove: bool = False
+ self,
+ key: Union[str, List[str]],
+ *,
+ only_first: bool = False,
+ remove: bool = False,
) -> Optional[Any]:
"""Get a property from the data.
@@ -103,6 +113,7 @@ def _get_property(
Args:
key: Name of the key or sequence of multiple keys to retrieve the value.
+ only_first: If True, returns only first entry if the value is a list.
remove: If True, will remove the retrieved value and clean up the dict.
"""
key_path = [key] if isinstance(key, str) else key
@@ -111,25 +122,28 @@ def _get_property(
seq = [curr]
for k in key_path:
curr = curr.get(k)
+ curr = curr[0] if isinstance(curr, list) and only_first else curr
seq.append(curr)
if curr is None:
return None
if remove:
seq.pop()
- logger.debug("remove in")
- logger.debug(seq[-1])
del seq[-1][key_path[-1]] # remove leaf value
# clean up the tree
for key, dct in reversed(list(zip(key_path[:-1], seq[:-1]))):
if not dct.get(key):
del dct[key]
+ if isinstance(curr, list) and only_first:
+ return curr[0]
return curr
def _set_property(self, key: Union[str, List[str], IgnoreKey], value: Any) -> None:
"""Set a property in the data.
+ Note if there are lists along the path, they are cleared out.
+
Override this to e.g. rewrite the retrieved key
(e.g. if everything relevant is in some subobject).
"""
@@ -147,6 +161,7 @@ def _set_property(self, key: Union[str, List[str], IgnoreKey], value: Any) -> No
if key not in curr:
curr[key] = {}
curr = curr[key]
+
curr[key_path[-1]] = value
# ----
@@ -271,8 +286,6 @@ def _to_person(person_obj: Any) -> Person:
@classmethod
def _parse_people(cls, people: Optional[List[Any]]) -> List[Person]:
"""Return a list of Persons parsed from list of format-specific people representations."""
- if not people:
- return []
return list(map(cls._to_person, people or []))
# ----
@@ -333,6 +346,17 @@ def maintainers(self, maintainers: List[Person]) -> None:
maintainers = [self._from_person(c) for c in maintainers]
self._set_property(self._get_key("maintainers"), maintainers)
+ @property
+ def contributors(self):
+ """Return the contributors of the project."""
+ return self._get_property(self._get_key("contributors"))
+
+ @contributors.setter
+ def contributors(self, contributors: List[Person]) -> None:
+ """Set the contributors of the project."""
+ contributors = [self._from_person(c) for c in contributors]
+ self._set_property(self._get_key("contributors"), contributors)
+
@property
def keywords(self) -> Optional[List[str]]:
"""Return the keywords of the project."""
diff --git a/src/somesy/package_json/writer.py b/src/somesy/package_json/writer.py
index 8dbf5fee..4382725e 100644
--- a/src/somesy/package_json/writer.py
+++ b/src/somesy/package_json/writer.py
@@ -2,7 +2,7 @@
import logging
from collections import OrderedDict
from pathlib import Path
-from typing import List, Optional
+from typing import Dict, List, Optional, Union
from rich.pretty import pretty_repr
@@ -112,5 +112,19 @@ def sync(self, metadata: ProjectMetadata) -> None:
"""
super().sync(metadata)
self.contributors = self._sync_person_list(self.contributors, metadata.people)
- if metadata.repository:
- self.repository = {"type": "git", "url": str(metadata.repository)}
+
+ @property
+ def repository(self) -> Optional[Union[str, Dict]]:
+ """Return the repository url of the project."""
+ if repo := super().repository:
+ if isinstance(repo, str):
+ return repo
+ else:
+ return repo.get("url")
+ else:
+ return None
+
+ @repository.setter
+ def repository(self, value: Optional[Union[str, Dict]]) -> None:
+ """Set the repository url of the project."""
+ self._set_property(self._get_key("repository"), dict(type="git", url=value))
diff --git a/src/somesy/pom_xml/__init__.py b/src/somesy/pom_xml/__init__.py
index a6f8fc52..b6d40446 100644
--- a/src/somesy/pom_xml/__init__.py
+++ b/src/somesy/pom_xml/__init__.py
@@ -1 +1,10 @@
"""Somesy implementation for Java Maven pom.xml."""
+
+# some POM-related constants and reusable objects
+POM_URL = "http://maven.apache.org/POM/4.0.0"
+POM_PREF = "{" + POM_URL + "}"
+POM_ROOT_ATRS = {
+ "xmlns": "http://maven.apache.org/POM/4.0.0",
+ "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
+ "xsi:schemaLocation": "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd",
+}
diff --git a/src/somesy/pom_xml/writer.py b/src/somesy/pom_xml/writer.py
new file mode 100644
index 00000000..271b67e5
--- /dev/null
+++ b/src/somesy/pom_xml/writer.py
@@ -0,0 +1,216 @@
+"""Writer adapter for pom.xml files."""
+import logging
+import xml.etree.ElementTree as ET
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Union
+
+from somesy.core.models import Person
+from somesy.core.writer import FieldKeyMapping, ProjectMetadataWriter
+
+from . import POM_ROOT_ATRS, POM_URL
+from .xmlproxy import XMLProxy
+
+logger = logging.getLogger("somesy")
+
+
+class POM(ProjectMetadataWriter):
+ """Java Maven pom.xml parser and saver."""
+
+ # TODO: write a wrapper for ElementTree that behaves like a dict
+ # TODO: set up correct field name mappings
+
+ def __init__(
+ self,
+ path: Path,
+ create_if_not_exists: bool = True,
+ ):
+ """Java Maven pom.xml parser.
+
+ See [somesy.core.writer.ProjectMetadataWriter.__init__][].
+ """
+ mappings: FieldKeyMapping = {
+ # "year": ["inceptionYear"], # not supported by somesy + does not really change
+ # "project_slug": ["artifactId"], # not supported by somesy for sync
+ "license": ["licenses", "license"],
+ "homepage": ["url"],
+ "repository": ["scm"],
+ "documentation": ["distributionManagement", "site"],
+ "authors": ["developers", "developer"],
+ "contributors": ["contributors", "contributor"],
+ }
+ super().__init__(
+ path, create_if_not_exists=create_if_not_exists, direct_mappings=mappings
+ )
+
+ def _init_new_file(self):
+ """Initialize new pom.xml file."""
+ pom = XMLProxy(ET.Element("project", POM_ROOT_ATRS))
+ pom["properties"] = {"info.versionScheme": "semver-spec"}
+ pom.write(self.path)
+
+ def _load(self):
+ """Load the POM file."""
+ ET.register_namespace("", POM_URL) # register POM as default xml namespace
+ self._data = XMLProxy.parse(self.path, default_namespace=POM_URL)
+
+ def _validate(self):
+ """Validate the POM file."""
+ logger.info("Cannot validate POM file, skipping validation.")
+
+ def save(self, path: Optional[Path] = None) -> None:
+ """Save the POM DOM to a file."""
+ self._data.write(path or self.path, default_namespace=None)
+
+ def _get_property(
+ self,
+ key: Union[str, List[str]],
+ *,
+ only_first: bool = False,
+ remove: bool = False,
+ ) -> Optional[Any]:
+ elem = super()._get_property(key, only_first=only_first, remove=remove)
+ if elem is not None:
+ if isinstance(elem, list):
+ return [e.to_jsonlike() for e in elem]
+ else:
+ return elem.to_jsonlike()
+ return None
+
+ @staticmethod
+ def _from_person(person: Person):
+ """Convert person object to dict for POM XML person format."""
+ ret: Dict[str, Any] = {}
+ person_id = str(person.orcid) or person.to_name_email_string()
+ ret["id"] = person_id
+ ret["name"] = person.full_name
+ ret["email"] = person.email
+ if person.orcid:
+ ret["url"] = str(person.orcid)
+ if person.contribution_types:
+ ret["roles"] = dict(role=[c.value for c in person.contribution_types])
+ return ret
+
+ @staticmethod
+ def _to_person(person_obj) -> Person:
+ """Parse POM XML person to a somesy Person."""
+ print(person_obj)
+ names = person_obj["name"].split()
+ gnames = " ".join(names[:-1])
+ fname = names[-1]
+ email = person_obj["email"]
+ url = person_obj.get("url")
+ maybe_orcid = url if url.find("orcid.org") >= 0 else None
+ if roles := person_obj.get("roles"):
+ contr = roles["role"]
+ else:
+ contr = None
+
+ return Person(
+ given_names=gnames,
+ family_names=fname,
+ email=email,
+ orcid=maybe_orcid,
+ contribution_types=contr,
+ )
+
+ # no search keywords supported in POM
+ @property
+ def keywords(self) -> Optional[List[str]]:
+ """Return the keywords of the project."""
+ pass
+
+ @keywords.setter
+ def keywords(self, keywords: List[str]) -> None:
+ """Set the keywords of the project."""
+ pass
+
+ # authors must be a list
+ @property
+ def authors(self):
+ """Return the authors of the project."""
+ authors = self._get_property(self._get_key("authors"))
+ return authors if isinstance(authors, list) else [authors]
+
+ @authors.setter
+ def authors(self, authors: List[Person]) -> None:
+ """Set the authors of the project."""
+ authors = [self._from_person(c) for c in authors]
+ self._set_property(self._get_key("authors"), authors)
+
+ # contributors must be a list
+ @property
+ def contributors(self):
+ """Return the contributors of the project."""
+ contr = self._get_property(self._get_key("contributors"))
+ if contr is None:
+ return []
+ return contr if isinstance(contr, list) else [contr]
+
+ @contributors.setter
+ def contributors(self, contributors: List[Person]) -> None:
+ """Set the contributors of the project."""
+ contr = [self._from_person(c) for c in contributors]
+ self._set_property(self._get_key("contributors"), contr)
+
+ # no maintainers supported im POM, only developers and contributors
+ @property
+ def maintainers(self):
+ """Return the maintainers of the project."""
+ return []
+
+ @maintainers.setter
+ def maintainers(self, maintainers: List[Person]) -> None:
+ """Set the maintainers of the project."""
+ pass
+
+ # only one project license supported in somesy (POM can have many)
+ @property
+ def license(self) -> Optional[str]:
+ """Return the license of the project."""
+ lic = self._get_property(self._get_key("license"), only_first=True)
+ return lic.get("name") if lic is not None else None
+
+ @license.setter
+ def license(self, license: Optional[str]) -> None:
+ """Set the license of the project."""
+ self._set_property(
+ self._get_key("license"), dict(name=license, distribution="repo")
+ )
+
+ @property
+ def repository(self) -> Optional[Union[str, dict]]:
+ """Return the repository url of the project."""
+ repo = super().repository
+ if isinstance(repo, str):
+ return repo
+ return repo.get("url") if repo is not None else None
+
+ @repository.setter
+ def repository(self, value: Optional[Union[str, dict]]) -> None:
+ """Set the repository url of the project."""
+ self._set_property(
+ self._get_key("repository"), dict(name="git repository", url=value)
+ )
+
+ @property
+ def documentation(self) -> Optional[Union[str, dict]]:
+ """Return the documentation url of the project."""
+ docs = super().documentation
+ if isinstance(docs, str):
+ return docs
+ return docs.get("url") if docs is not None else None
+
+ @documentation.setter
+ def documentation(self, value: Optional[Union[str, dict]]) -> None:
+ """Set the documentation url of the project."""
+ self._set_property(
+ self._get_key("documentation"), dict(name="documentation site", url=value)
+ )
+
+ def sync(self, metadata) -> None:
+ """Sync codemeta.json with project metadata.
+
+ Use existing sync function from ProjectMetadataWriter but update repository and contributors.
+ """
+ super().sync(metadata)
+ self.contributors = self._sync_person_list(self.contributors, metadata.people)
diff --git a/src/somesy/pom_xml/xmlproxy.py b/src/somesy/pom_xml/xmlproxy.py
new file mode 100644
index 00000000..7b2586a4
--- /dev/null
+++ b/src/somesy/pom_xml/xmlproxy.py
@@ -0,0 +1,407 @@
+"""Wrapper to provide dict-like access to XML via ElementTree."""
+from __future__ import annotations
+
+import xml.etree.ElementTree as ET
+from pathlib import Path
+from typing import Any, List, Optional, Union, cast
+
+import defusedxml.ElementTree as DET
+
+# shallow type hint mostly for documentation purpose
+JSONLike = Any
+
+
+def load_xml(path: Path) -> ET.ElementTree:
+ """Parse an XML file into an ElementTree, preserving comments."""
+ path = path if isinstance(path, Path) else Path(path)
+ parser = DET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
+ return DET.parse(path, parser=parser)
+
+
+def indent(elem, level=0):
+ """Indent the elements of this XML node (i.e. pretty print)."""
+ i = "\n" + level * " "
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ for el in elem:
+ indent(el, level + 1)
+ if not el.tail or not el.tail.strip():
+ el.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
+
+
+class XMLProxy:
+ """Class providing dict-like access to edit XML via ElementTree.
+
+ Note that this wrapper facade is limited to a restricted (but useful) subset of XML:
+ * XML attributes are not supported
+ * DTDs are ignored (arbitrary keys can be queried and added)
+ * each tag is assumed to EITHER contain text OR more nested tags
+ * lists are treated atomically (no way to add/remove element from a collection)
+
+ The semantics is implemented as follows:
+
+ * If there are multiple tags with the same name, a list of XMLProxy nodes is returned
+ * If a unique tag does have no nested tags, its `text` string value is returned
+ * Otherwise, the node is returned
+ """
+
+ def _wrap(self, el: ET.Element) -> XMLProxy:
+ """Wrap a different element, inheriting the same namespace."""
+ return XMLProxy(el, default_namespace=self._def_ns)
+
+ def _dump(self):
+ """Dump XML to stdout (for debugging)."""
+ ET.dump(self._node)
+
+ def _qualified_key(self, key: str):
+ """If passed key is not qualified, prepends the default namespace (if set)."""
+ if key[0] == "{" or not self._def_ns:
+ return key
+ return "{" + self._def_ns + "}" + key
+
+ def _shortened_key(self, key: str):
+ """Inverse of `_qualified_key` (strips default namespace from element name)."""
+ if key[0] != "{" or not self._def_ns or key.find(self._def_ns) < 0:
+ return key
+ return key[key.find("}") + 1 :]
+
+ # ----
+
+ def __init__(self, el: ET.Element, *, default_namespace: Optional[str] = None):
+ """Wrap an existing XML ElementTree Element."""
+ self._node: ET.Element = el
+ self._def_ns = default_namespace
+
+ @classmethod
+ def parse(cls, path: Union[str, Path], **kwargs) -> XMLProxy:
+ """Parse an XML file into a wrapped ElementTree, preserving comments."""
+ path = path if isinstance(path, Path) else Path(path)
+ return cls(load_xml(path).getroot(), **kwargs)
+
+ def write(self, path: Union[str, Path], *, header: bool = True, **kwargs):
+ """Write the XML DOM to an UTF-8 encoded file."""
+ path = path if isinstance(path, Path) else Path(path)
+ et = ET.ElementTree(self._node)
+ if self._def_ns and "default_namespace" not in kwargs:
+ kwargs["default_namespace"] = self._def_ns
+ indent(et.getroot())
+ et.write(path, encoding="UTF-8", xml_declaration=header, **kwargs)
+
+ def __repr__(self):
+ """See `object.__repr__`."""
+ return str(self._node)
+
+ def __len__(self):
+ """Return number of inner tags inside current XML element.
+
+ Note that bool(node) thus checks whether an XML node is a leaf in the element tree.
+ """
+ return len(self._node)
+
+ def __iter__(self):
+ """Iterate the nested elements in-order."""
+ return map(self._wrap, iter(self._node))
+
+ @property
+ def namespace(self) -> Optional[str]:
+ """Default namespace of this node."""
+ return self._def_ns
+
+ @property
+ def is_comment(self):
+ """Return whether the current element node is an XML comment."""
+ return not isinstance(self._node.tag, str)
+
+ @property
+ def tag(self) -> Optional[str]:
+ """Return tag name of this element (unless it is a comment)."""
+ if self.is_comment:
+ return None
+ return self._shortened_key(self._node.tag)
+
+ @tag.setter
+ def tag(self, val: str):
+ """Set the tag of this element."""
+ if self.is_comment:
+ raise ValueError("Cannot set tag name for comment element!")
+ self._node.tag = self._qualified_key(val)
+
+ # ---- helpers ----
+
+ def to_jsonlike(
+ self,
+ *,
+ strip_default_ns: bool = True,
+ keep_root: bool = False,
+ ) -> JSONLike:
+ """Convert XML node to a JSON-like primitive, array or dict (ignoring attributes).
+
+ Note that all leaf values are strings (i.e. not parsed to bool/int/float etc.).
+
+ Args:
+ strip_default_ns: Do not qualify keys from the default namespace
+ keep_root: If true, the root tag name will be preserved (`{"root_tag": {...}}`)
+ """
+ if not len(self): # leaf -> assume it's a primitive value
+ return self._node.text or ""
+
+ dct = {}
+ ccnt = 0
+ for elem in iter(self):
+ raw = elem._node
+ if not isinstance(raw.tag, str):
+ ccnt += 1
+ key = f"__comment_{ccnt}__"
+ else:
+ key = raw.tag if not strip_default_ns else self._shortened_key(raw.tag)
+
+ curr_val = elem.to_jsonlike(strip_default_ns=strip_default_ns)
+ if key not in dct:
+ dct[key] = curr_val
+ continue
+ val = dct[key]
+ if not isinstance(val, list):
+ dct[key] = [dct[key]]
+ dct[key].append(curr_val)
+
+ return dct if not keep_root else {self._shortened_key(self._node.tag): dct}
+
+ @classmethod
+ def _from_jsonlike_primitive(
+ cls, val, *, elem_name: Optional[str] = None, **kwargs
+ ) -> Union[str, XMLProxy]:
+ """Convert a leaf node into a string value (i.e. return inner text).
+
+ Returns a string (or an XML element, if elem_name is passed).
+ """
+ if val is None:
+ ret = "" # turn None into empty string
+ elif isinstance(val, str):
+ ret = val
+ elif isinstance(val, bool):
+ ret = str(val).lower() # True -> true / False -> false
+ elif isinstance(val, (int, float)):
+ ret = str(val)
+ else:
+ raise TypeError(
+ f"Value of type {type(val)} is not JSON-like primitive: {val}"
+ )
+
+ if not elem_name:
+ return ret
+ else: # return the value wrapped as an element (needed in from_jsonlike)
+ elem = ET.Element(elem_name)
+ elem.text = ret
+ return cls(elem, **kwargs)
+
+ @classmethod
+ def from_jsonlike(
+ cls, val: JSONLike, *, root_name: Optional[str] = None, **kwargs: Any
+ ):
+ """Convert a JSON-like primitive, array or dict into an XML element.
+
+ Note that booleans are serialized as `true`/`false` and None as `null`.
+
+ Args:
+ val: Value to convert into an XML element.
+ root_name: If `val` is a dict, defines the tag name for the root element.
+ kwargs: Additional arguments for XML element instantiation.
+ """
+ if isinstance(val, list):
+ return list(
+ map(lambda x: cls.from_jsonlike(x, root_name=root_name, **kwargs), val)
+ )
+ if not isinstance(val, dict): # primitive val
+ return cls._from_jsonlike_primitive(val, elem_name=root_name, **kwargs)
+
+ # now the dict case remains
+ elem = ET.Element(root_name or "root")
+ for k, v in val.items():
+ if k.startswith(
+ "__comment_"
+ ): # special key names are mapped to XML comments
+ elem.append(ET.Comment(v if isinstance(v, str) else str(v)))
+
+ elif isinstance(v, list):
+ for vv in XMLProxy.from_jsonlike(v, root_name=k, **kwargs):
+ elem.append(vv._node)
+ elif not isinstance(v, dict): # primitive val
+ # FIXME: use better case-splitting for type of function to avoid cast
+ tmp = cast(
+ XMLProxy,
+ XMLProxy._from_jsonlike_primitive(v, elem_name=k, **kwargs),
+ )
+ elem.append(tmp._node)
+ else: # dict
+ elem.append(XMLProxy.from_jsonlike(v, root_name=k)._node)
+
+ return cls(elem, **kwargs)
+
+ # ---- dict-like access ----
+
+ def get(self, key: str, *, as_nodes: bool = False, deep: bool = False):
+ """Get sub-structure(s) of value(s) matching desired XML tag name.
+
+ * If there are multiple matching elements, will return them all as a list.
+ * If there is a single matching element, will return that element without a list.
+
+ Args:
+ key: tag name to retrieve
+ as_nodes: If true, will *always* return a list of (zero or more) XML nodes
+ deep: Expand nested XML elements instead of returning them as XML nodes
+ """
+ # NOTE: could allow to retrieve comments when using empty string/none as key?
+
+ if as_nodes and deep:
+ raise ValueError("as_nodes and deep are mutually exclusive!")
+ if not key:
+ raise ValueError("Key must not be an empty string!")
+ key = self._qualified_key(key)
+
+ # if not fully qualified + default NS is given, use it for query
+ lst = self._node.findall(key)
+ ns: List[XMLProxy] = list(map(self._wrap, lst))
+ if as_nodes: # return it as a list of xml nodes
+ return ns
+ if not ns: # no element
+ return None
+
+ ret = ns if not deep else [x.to_jsonlike() for x in ns]
+ if len(ret) == 1:
+ return ret[0] # single element
+ else:
+ return ret
+
+ def __getitem__(self, key: str):
+ """Acts like `dict.__getitem__`, implemented with `get`."""
+ val = self.get(key)
+ if val is not None:
+ return val
+ else:
+ raise KeyError(key)
+
+ def __contains__(self, key: str) -> bool:
+ """Acts like `dict.__contains__`, implemented with `get`."""
+ return self.get(key) is not None
+
+ def __delitem__(self, key: Union[str, XMLProxy]):
+ """Delete a nested XML element with matching key name.
+
+ Note that **all** XML elements with the given tag name are removed!
+
+ To prevent this behavior, instead of a string tag name you can provide the
+ exact element to be removed, i.e. if a node `node_a` represents the following XML:
+
+ ```
+
+ 1
+ 2
+ 3
+
+ ```
+
+ Then we have that:
+
+ * `del node_a["b"]` removes **both** tags, leaving just the `c` tag.
+ * `del node_a[node_a["a"][1]]` removes just the second tag with the `3`.
+ """
+ if isinstance(key, str):
+ nodes = self.get(key, as_nodes=True)
+ else:
+ nodes = [key] if key._node in self._node else []
+
+ if not nodes:
+ raise KeyError(key)
+
+ if self._node.text is not None:
+ self._node.text = ""
+ for child in nodes:
+ self._node.remove(child._node)
+
+ def _clear(self):
+ """Remove contents of this XML element (e.g. for overwriting in-place)."""
+ self._node.text = ""
+ children = list(iter(self._node)) # need to store, removal invalidates iterator
+ for child in children:
+ self._node.remove(child)
+
+ def __setitem__(self, key: Union[str, XMLProxy], val: Union[JSONLike, XMLProxy]):
+ """Add or overwrite an inner XML tag.
+
+ If there is exactly one matching tag, the value is substituted in-place.
+ If the passed value is a list, all list entries are added in their own element.
+
+ If there are multiple existing matches or target values, then
+ **all** existing elements are removed and the new value(s) are added in
+ new element(s) (i.e. coming after other unrelated existing elements)!
+
+ To prevent this behavior, instead of a string tag name you can provide the
+ exact element to be overwritten, i.e. if a node `node_a` represents the following XML:
+
+ ```
+
+ 1
+ 2
+ 3
+
+ ```
+
+ Then we have that:
+
+ * `node_a["b"] = 5` removes both existing tags and creates a new tag with the passed value(s).
+ * `node_a[node_a["b"][1]] = 5` replaces the `3` in the second tag with the `5`.
+
+ Note that the passed value must be either an XML element already, or be a pure JSON-like object.
+ """
+ if isinstance(key, str):
+ nodes = self.get(key, as_nodes=True)
+ # delete all existing elements if multiple exist or are passed
+ if len(nodes) > 1 or (len(nodes) and isinstance(val, list)):
+ del self[key]
+ nodes = []
+ # now we can assume there's zero or one suitable target elements
+ if nodes: # if it is one, clear it out
+ nodes[0]._clear()
+ else: # an XMLProxy object was passed as key -> try to use that
+ if isinstance(val, list):
+ raise ValueError(
+ "Cannot overwrite a single element with a list of values!"
+ )
+ # ensure the target node is cleared out and use it as target
+ key._clear()
+ nodes = [key]
+ key = key.tag
+
+ # ensure key string is qualified with a namespace
+ key_name: str = self._qualified_key(key)
+
+ # normalize passed value(s) to be list (general case)
+ vals = val if isinstance(val, list) else [val]
+
+ # ensure there is the required number of target element nodes
+ for _ in range(len(vals) - len(nodes)):
+ nodes.append(self._wrap(ET.SubElement(self._node, key_name)))
+
+ # normalize values no XML element nodes
+ nvals = []
+ for val in vals:
+ # ensure value is represented as an XML node
+ if isinstance(val, XMLProxy):
+ obj = self._wrap(ET.Element("dummy"))
+ obj._node.append(val._node)
+ else:
+ obj = self.from_jsonlike(val, root_name=key_name)
+
+ nvals.append(obj)
+
+ for node, val in zip(nodes, nvals):
+ # transplant node contents into existing element (so it is inserted in-place)
+ node._node.text = val._node.text
+ for child in iter(val):
+ node._node.append(child._node)
diff --git a/src/somesy/pyproject/writer.py b/src/somesy/pyproject/writer.py
index abfe148e..742e46f3 100644
--- a/src/somesy/pyproject/writer.py
+++ b/src/somesy/pyproject/writer.py
@@ -56,12 +56,12 @@ def save(self, path: Optional[Path] = None) -> None:
tomlkit.dump(self._data, f)
def _get_property(
- self, key: Union[str, List[str]], *, remove: bool = False
+ self, key: Union[str, List[str]], *, remove: bool = False, **kwargs
) -> Optional[Any]:
"""Get a property from the pyproject.toml file."""
key_path = [key] if isinstance(key, str) else key
full_path = self._section + key_path
- return super()._get_property(full_path, remove=remove)
+ return super()._get_property(full_path, remove=remove, **kwargs)
def _set_property(self, key: Union[str, List[str], IgnoreKey], value: Any) -> None:
"""Set a property in the pyproject.toml file."""
diff --git a/tests/conftest.py b/tests/conftest.py
index 65cb237d..364ce112 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,6 +11,12 @@
from somesy.pyproject import Pyproject
from somesy.julia import Julia
from somesy.fortran import Fortran
+from somesy.pom_xml.writer import POM
+
+TEST_DIR = Path(__file__).resolve().parent
+
+TEST_DATA_DIR = TEST_DIR / "data"
+"""Location of the test input data."""
class FileTypes(Enum):
@@ -21,6 +27,7 @@ class FileTypes(Enum):
PACKAGE_JSON = "package_json"
JULIA = "julia"
FORTRAN = "fortran"
+ POM_XML = "pom_xml"
@pytest.fixture(scope="session", autouse=True)
@@ -76,6 +83,8 @@ def _create_files(files: Set[Tuple[FileTypes, str]]):
read_file_name = read_file_path / Path("Project.toml")
elif file_type == FileTypes.FORTRAN:
read_file_name = read_file_path / Path("fpm.toml")
+ elif file_type == FileTypes.POM_XML:
+ read_file_name = read_file_path / Path("pom.xml")
with open(read_file_name, "r") as f:
content = f.read()
@@ -105,7 +114,7 @@ def _load_files(files: Set[FileTypes]):
if not isinstance(file_type, FileTypes):
raise ValueError(f"Invalid file type: {file_type}")
- read_file_name = Path("tests/data")
+ read_file_name = TEST_DATA_DIR
if file_type == FileTypes.CITATION:
read_file_name = read_file_name / Path("CITATION.cff")
file_instances[file_type] = CFF(read_file_name)
@@ -127,6 +136,9 @@ def _load_files(files: Set[FileTypes]):
elif file_type == FileTypes.FORTRAN:
read_file_name = read_file_name / Path("fpm.toml")
file_instances[file_type] = Fortran(read_file_name)
+ elif file_type == FileTypes.POM_XML:
+ read_file_name = read_file_name / Path("pom.xml")
+ file_instances[file_type] = POM(read_file_name)
return file_instances
@@ -135,6 +147,7 @@ def _load_files(files: Set[FileTypes]):
@pytest.fixture
def person() -> Person:
+ """Return example person."""
p = {
"given-names": "Jane",
"email": "j.doe@example.com",
@@ -144,3 +157,13 @@ def person() -> Person:
ret = Person.model_validate(p)
ret.set_key_order(list(p.keys())) # custom order!
return ret
+
+
+@pytest.fixture
+def xml_examples():
+ """Return path for an xml example file in test directory, based on file name."""
+
+ def _xml_loader(filename: str) -> Path:
+ return TEST_DATA_DIR / filename
+
+ yield _xml_loader
diff --git a/tests/data/blank_pom.xml b/tests/data/blank_pom.xml
new file mode 100644
index 00000000..c8ac1aa9
--- /dev/null
+++ b/tests/data/blank_pom.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/example_1.xml b/tests/data/example_1.xml
new file mode 100644
index 00000000..20f7dce0
--- /dev/null
+++ b/tests/data/example_1.xml
@@ -0,0 +1,17 @@
+
+
+
+ foo
+
+ true
+
+ 42
+
+
+ x
+ y
+
+ z
+
+ 3.14
+
diff --git a/tests/data/example_pom.xml b/tests/data/example_pom.xml
new file mode 100644
index 00000000..17af66a3
--- /dev/null
+++ b/tests/data/example_pom.xml
@@ -0,0 +1,37 @@
+
+
+ ACME Tools
+ acme-tools
+ The ACME tools.
+ https://github.com/lampepfl/dotty
+ 0.1.0
+
+ semver-spec
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0
+ repo
+
+
+
+ https://github.com/lampepfl/dotty
+ scm:git:git@github.com:lampepfl/dotty.git
+
+
+
+ odersky
+ Martin Odersky
+ https://github.com/odersky
+ martin.odersky@epfl.ch
+
+
+ DarkDimius
+ Dmitry Petrashko
+ https://d-d.me
+ me@d-d.me
+
+
+
diff --git a/tests/data/pom.xml b/tests/data/pom.xml
new file mode 100644
index 00000000..de4a9731
--- /dev/null
+++ b/tests/data/pom.xml
@@ -0,0 +1,40 @@
+
+
+ test-package
+ test-package
+ This is a test package for demonstration purposes.
+ https://github.com/lampepfl/dotty
+ 0.1.0
+
+ semver-spec
+
+
+
+
+ MIT
+ repo
+
+
+ Apache-2.0
+ repo
+
+
+
+ https://github.com/lampepfl/dotty
+ scm:git:git@github.com:lampepfl/dotty.git
+
+
+
+ jane
+ Jane Doe
+ https://github.com/janedoe
+ jane.doe@acme.com
+
+ maintenance
+ code
+ test
+ review
+
+
+
+
diff --git a/tests/output/test_codemeta.py b/tests/output/test_codemeta.py
index 882d3964..2f95d933 100644
--- a/tests/output/test_codemeta.py
+++ b/tests/output/test_codemeta.py
@@ -1,11 +1,11 @@
-from somesy.codemeta import Codemeta
+from somesy.codemeta import CodeMeta
from somesy.json_wrapper import json
def test_update_codemeta(somesy_input, tmp_path):
codemeta_file = tmp_path / "codemeta.json"
- cm = Codemeta(codemeta_file)
+ cm = CodeMeta(codemeta_file)
# firstly, create a codemeta file
cm.sync(somesy_input.project)
diff --git a/tests/output/test_pom_writer.py b/tests/output/test_pom_writer.py
new file mode 100644
index 00000000..2b0ed98d
--- /dev/null
+++ b/tests/output/test_pom_writer.py
@@ -0,0 +1,127 @@
+import pytest
+
+from somesy.pom_xml.writer import POM
+from somesy.core.models import LicenseEnum, Person, ProjectMetadata
+
+
+@pytest.fixture
+def pom(load_files, file_types):
+ files = load_files([file_types.POM_XML])
+ return files[file_types.POM_XML]
+
+
+@pytest.fixture
+def pom_file(create_files, file_types):
+ return create_files([(file_types.POM_XML, "pom.xml")]) / "pom.xml"
+
+
+def test_content_match(pom: POM):
+ assert pom.name == "test-package"
+ assert pom.description == "This is a test package for demonstration purposes."
+ assert pom.license == "MIT"
+ assert len(pom.authors) == 1
+
+
+def test_sync(pom: POM, somesy_input: dict):
+ pom.sync(somesy_input.project)
+ assert pom.name == "testproject"
+ assert pom.version == "1.0.0"
+
+
+def test_save(tmp_path):
+ # test save with default path
+ file_path = tmp_path / "pom.xml"
+ pom = POM(file_path, create_if_not_exists=True)
+ pom.save()
+ assert file_path.is_file()
+
+ # test save with custom path
+ custom_path = tmp_path / "custom.xml"
+ pom.save(custom_path)
+ assert custom_path.is_file()
+
+
+def test_from_to_person(person: Person):
+ p1 = POM._from_person(person)
+ assert p1["name"] == person.full_name
+ assert p1["email"] == person.email
+ assert p1["url"] == str(person.orcid)
+
+ p = POM._to_person(POM._from_person(person))
+ assert p.full_name == person.full_name
+ assert p.email == person.email
+ assert str(p.orcid) == str(person.orcid)
+
+
+def test_person_merge(pom_file, person: Person):
+ pj = POM(pom_file)
+
+ pm = ProjectMetadata(
+ name="My awesome project",
+ description="Project description",
+ license=LicenseEnum.MIT,
+ version="0.1.0",
+ people=[person.model_copy(update=dict(author=True, publication_author=True))],
+ )
+ pj.sync(pm)
+ pj.save()
+
+ # jane becomes john -> modified person
+ person1b = person.model_copy(
+ update={"given_names": "John", "author": True, "publication_author": True}
+ )
+
+ # different Jane Doe with different orcid -> new person
+ person2 = person.model_copy(
+ update={
+ "orcid": "https://orcid.org/4321-0987-3231",
+ "email": "i.am.jane@doe.com",
+ "author": True,
+ "publication_author": True,
+ }
+ )
+ # use different order, just for some difference
+ person2.set_key_order(["given_names", "orcid", "family_names", "email"])
+
+ # listed in "arbitrary" order in somesy metadata (new person comes first)
+ pm.people = [person2, person1b] # need to assign like that to keep _key_order
+ pj.sync(pm)
+ pj.save()
+
+ # existing author order preserved
+ assert pj.authors[0]["name"] == person1b.full_name
+ assert pj.authors[0]["email"] == person1b.email
+
+ assert pj.contributors[1]["name"] == person2.full_name
+ assert pj.contributors[1]["email"] == person2.email
+
+ # new person
+ person3 = Person(
+ **{
+ "given_names": "Janice",
+ "family_names": "Doethan",
+ "email": "jane93@gmail.com",
+ "author": True,
+ "publication_author": True,
+ }
+ )
+ # john has a new email address
+ person1c = person1b.model_copy(update={"email": "john.of.us@qualityland.com"})
+ # jane 2 is removed from authors, but added to maintainers
+ person2.author = False
+ person2.publication_author = False
+ person2.maintainer = True
+ # reflect in project metadata
+ pm.people = [person3, person2, person1c]
+ # sync to CFF file
+ pj.sync(pm)
+ pj.save()
+
+ assert len(pj.contributors) == 3
+ assert len(pj.maintainers) == 0
+ assert pj.authors[0]["name"] == person1c.full_name
+ assert pj.authors[0]["email"] == person1c.email
+ print(pj.contributors)
+
+ assert pj.contributors[2]["name"] == person3.full_name
+ assert pj.contributors[2]["email"] == person3.email
diff --git a/tests/processing/test_xmlwrapper.py b/tests/processing/test_xmlwrapper.py
new file mode 100644
index 00000000..d41c5c56
--- /dev/null
+++ b/tests/processing/test_xmlwrapper.py
@@ -0,0 +1,168 @@
+from somesy.pom_xml import POM_URL
+from somesy.pom_xml.xmlproxy import XMLProxy
+
+import pytest
+
+
+def test_parse_write(tmp_path, xml_examples):
+ """Make sure that header + namespace of XML are set correctly and comments are preserved."""
+ file_src = xml_examples("blank_pom.xml")
+ file_trg = tmp_path / "written.xml"
+
+ prx = XMLProxy.parse(file_src, default_namespace=POM_URL)
+
+ # the root
+ assert repr(prx).startswith(
+ " returns list
+ lst = prx["listEntry"]
+ assert len(lst) == 2
+ assert lst[0].to_jsonlike() == "foo"
+ assert lst[1].to_jsonlike() == {"someDict": {"a": "x", "b": "y"}, "someValue": "z"}
+
+ expected = dict(
+ nothing="",
+ emptyString="",
+ listEntry=["foo", {"someDict": {"a": "x", "b": "y"}, "someValue": "z"}],
+ __comment_1__=" comment 1 ",
+ aBool="true",
+ __comment_2__=" comment 2 ",
+ anInt="42",
+ aFloat="3.14",
+ )
+ dct = prx.to_jsonlike()
+ assert dct == expected
+ assert list(dct.keys()) == list(expected.keys()) # key order correct
+
+
+def test_del_set_items(xml_examples):
+ prx = XMLProxy.parse(xml_examples("example_1.xml"), default_namespace=POM_URL)
+
+ # remove existing elements
+ del prx["emptyString"]
+ del prx["listEntry"][1]["someDict"]["b"]
+
+ with pytest.raises(KeyError):
+ del prx["emptyString"] # already removed
+ with pytest.raises(KeyError):
+ del prx["invalid"] # not existing
+
+ # add new elements
+ prx["newValue"] = 5
+ assert prx["newValue"].to_jsonlike() == "5"
+ dct = dict(some="hello", keys="world")
+ prx["newObject"] = dct
+ assert prx["newObject"].to_jsonlike() == dct
+
+ # overwrite existing elements
+ prx["anInt"] = 123 # primitive to primitive
+ assert prx["anInt"].to_jsonlike() == "123"
+ prx["newValue"] = dict(x=1, y=2) # primitive to complex
+ assert prx["newValue"].to_jsonlike() == dict(x="1", y="2")
+ prx["newValue"] = None # complex to primitive
+ assert prx["newValue"].to_jsonlike() == ""
+ prx["listEntry"][1]["someDict"]["a"] = "qux" # overwrite nested
+ assert prx["listEntry"][1]["someDict"]["a"].to_jsonlike() == "qux"
+
+ # overwrite specific if there are many hits
+ prx[prx["listEntry"][0]] = "xyz"
+ assert prx["listEntry"][0].to_jsonlike() == "xyz"
+ prx[prx["listEntry"][0]] = dict(x="1", y="2")
+ assert prx["listEntry"][0].to_jsonlike() == dict(x="1", y="2")
+ # overwrite all elements if there are many hits
+ prx["listEntry"] = 321
+ assert prx["listEntry"].to_jsonlike() == "321"
+ # overwrite all elements if there are many target values
+ prx["listEntry"] = [123, 456]
+ assert prx.get("listEntry", deep=True) == ["123", "456"]
+
+ # elements still in right order
+ exp_keys = ["nothing", None, "aBool", None, "anInt", "aFloat"]
+ exp_keys += ["newValue", "newObject", "listEntry", "listEntry"]
+ keys = [x.tag for x in iter(prx)]
+ assert keys == exp_keys
+
+
+def test_update_write(xml_examples, tmp_path):
+ prx = XMLProxy.parse(xml_examples("example_pom.xml"), default_namespace=POM_URL)
+
+ prx["version"] = "0.2.0"
+ prx["licenses"]["license"] = "MIT"
+ prx["developers"]["developer"][1]["name"] = "John Doe"
+ prx["newKey"] = 42
+ externalKey = "{https://something.com}externalKey"
+ prx[externalKey] = "value"
+
+ tmp_file = tmp_path / "written.xml"
+ prx.write(tmp_file)
+
+ # check that the comment is retained
+ content = tmp_file.read_text()
+ assert content.find("some comment") > 0
+ # check that the changes persisted
+ prs = XMLProxy.parse(tmp_file, default_namespace=POM_URL)
+ assert prs["developers"]["developer"][1]["name"].to_jsonlike() == "John Doe"
+ assert prs["version"].to_jsonlike() == "0.2.0"
+ assert "newKey" in prs
+ # check that the namespace of the external key is preserved
+ assert "externalKey" not in prs
+ assert externalKey in prs